Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use fsnotify to detect model/policy files change in casbin plugin #614

Merged
merged 40 commits into from
Jul 17, 2024

Conversation

lyt122
Copy link
Contributor

@lyt122 lyt122 commented Jun 27, 2024

To fix #556

Copy link

codecov bot commented Jun 27, 2024

Codecov Report

Attention: Patch coverage is 72.83951% with 22 lines in your changes missing coverage. Please review.

Project coverage is 88.19%. Comparing base (e27e2f7) to head (ac85bd9).
Report is 7 commits behind head on main.

Files Patch % Lines
plugins/pkg/file/fs.go 75.51% 7 Missing and 5 partials ⚠️
plugins/plugins/casbin/config.go 70.96% 5 Missing and 4 partials ⚠️
plugins/plugins/casbin/filter.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #614      +/-   ##
==========================================
- Coverage   88.36%   88.19%   -0.17%     
==========================================
  Files         115      115              
  Lines        5456     5457       +1     
==========================================
- Hits         4821     4813       -8     
- Misses        454      461       +7     
- Partials      181      183       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@spacewander spacewander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you update

time.Sleep(10 * time.Second) // TODO remove this once we switch the file change detector to inotify
to see if the change is detected?

fs = &Fsnotify{
Watcher: watcher,
}
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this return?

return
}

watcher := newFsnotify().Watcher
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we use defaultFsnotify?

plugins/pkg/file/fs.go Outdated Show resolved Hide resolved
plugins/pkg/file/fs.go Outdated Show resolved Hide resolved
f.reloadEnforcer()
}, conf.modelFile, conf.policyFile)
if err != nil {
api.LogErrorf("failed to update Enforcer: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The msg is not precise. The code above doesn't update Enforcer

plugins/pkg/file/fs.go Outdated Show resolved Hide resolved
if !ok {
return
}
logger.Info(fmt.Sprintf("event: %v", event))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just put the event as an argument is enough, we don't need to use Sprintf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I use logger.Info("event: %v", event), an error is occurred:
DPANIC file file/fs.go:100 odd number of arguments passed as key-value pairs for logging {"ignored key": "WRITE \"example3760250304\""}
so I use Sprintf.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the error message indicated, the arguments should be key-value pairs, which have even numbers, for example:

logger.Info("register plugin type", "name", name)

}
logger.Info(fmt.Sprintf("event: %v", event))
onChange()
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep watching, and should not exit after the first event. This is a waste of goroutine and watcher, and force the user to rewatch each time.

tmpfile.Close()
assert.False(t, IsChanged(f))
tmpfile.Sync()
mu.Lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the lock here guarantee the callback in WatchFiles run before assertion? The assertion may get the lock first.

logger.Error(err, "failed to close fsnotify watcher")
}
}(w)
err := w.Add(files.Name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider a case: people remove the file and then create a new file with the same name. The fsnotify won't be triggered in this case. What about watching the file's directory and filtering according to the name?

@spacewander
Copy link
Member

I think about it again. Using global fsnotify is tough for a beginner. We can implement a per-file fsnotify version first.

)

func IsChanged(files ...*File) bool {
func Update(onChange func(), files ...*File) (err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Update is redundant, use WatchFiles is enough.

@lyt122 lyt122 changed the title feat: Use fsnotify to detect model/policy files change in casbin plugin [WIP]feat: Use fsnotify to detect model/policy files change in casbin plugin Jun 30, 2024
@github-actions github-actions bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Jul 3, 2024
@spacewander spacewander marked this pull request as draft July 4, 2024 06:05
@spacewander
Copy link
Member

@lyt122
I converted this PR to draft, feel free to click the "Ready for review" button or @ me if you are ready.

@lyt122 lyt122 marked this pull request as ready for review July 8, 2024 14:14
@lyt122 lyt122 changed the title [WIP]feat: Use fsnotify to detect model/policy files change in casbin plugin feat: Use fsnotify to detect model/policy files change in casbin plugin Jul 8, 2024
@lyt122 lyt122 reopened this Jul 10, 2024
@github-actions github-actions bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Jul 10, 2024
@lyt122
Copy link
Contributor Author

lyt122 commented Jul 11, 2024

@spacewander

}
}()
if getChanged() {
conf.reloadEnforcer()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we call this inside file.WatchFiles(func() {?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because if reloadenforcer called in file.WatchFiles(), the config won't be changed. So I need the flag

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why. Could you explain it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. In fact, this problem bother me for a long time.

@@ -35,33 +35,27 @@ type filter struct {
config *config
}

var (
Changed = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this flag if we can do reloadEnforcer in the fsnotify's callback

ChangedMu.Unlock()
role, _ := headers.Get(conf.Token.Name) // role can be ""
url := headers.Url()
err := file.WatchFiles(func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we move this part inside config.Init?

// configuration is not changed, but file changed
err = os.WriteFile(policyFile2.Name(), []byte(policy), 0755)
require.Nil(t, err)

//wait to run reloadEnforcer
time.Sleep(5 * time.Second)
Copy link
Member

@spacewander spacewander Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping @lyt122
Don't miss #614 (comment)

hdr := http.Header{}
hdr.Set("customer", "alice")

assert.Eventually(t, func() bool {
resp, _ := dp.Post("/echo", hdr, strings.NewReader("any"))
return resp != nil && resp.StatusCode == 200
}, 1*time.Second, 10*time.Millisecond)
}, 3*time.Second, 1*time.Second)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, 3*time.Second, 1*time.Second)
}, 3*time.Second, 100*time.Millisecond)

Better to use shorter interval to reduce test time

@@ -88,6 +88,7 @@ func TestCasbin(t *testing.T) {
assert.Equal(t, tt.status, 0)
} else {
assert.Equal(t, tt.status, lr.Code)
assert.False(t, Changed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Changed is removed, we can compare the field in conf directly for test

func watchFiles(onChanged func(), file *File) {
dir := filepath.Dir(file.Name)
defer func() {
storeWatchedFiles.lock.Lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global storeWatchedFiles makes things complex. And the current impl is incorrect if multiple config watch the same file.

We can improve it like this:

  • create a watcher which wraps the fsnotify when initing the config
  • add files into the watcher
  • start the watcher (remember to stop the goroutine once the watcher is closed)

@lyt122
Copy link
Contributor Author

lyt122 commented Jul 12, 2024

@spacewander ready to review

runtime.SetFinalizer(conf, func(conf *config) {
err := conf.watcher.Stop()
if err != nil {
api.LogErrorf("failed to close watcher, err: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
api.LogErrorf("failed to close watcher, err: %v", err)
api.LogErrorf("failed to stop watcher, err: %v", err)

e, err := casbin.NewEnforcer(conf.Rule.Model, conf.Rule.Policy)
conf.watcher = watcher

err = conf.watcher.AddFile(conf.modelFile, conf.policyFile)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to name it AddFiles if this method accept multiple files

return defaultFs.Stat(path)
func (w *Watcher) Start(onChanged func()) {
go func() {
logger.Info("start watch files")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.Info("start watch files")
logger.Info("start watching files")

plugins/pkg/file/fs.go Outdated Show resolved Hide resolved
plugins/pkg/file/fs_test.go Show resolved Hide resolved
for {
select {
case event, ok := <-w.watcher.Events:
if !ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to add this branch as the watcher doesn't close the Events.

logger.Info("file changed: ", "event", event)
onChanged()
case err, ok := <-w.watcher.Errors:
if !ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

@lyt122
Copy link
Contributor Author

lyt122 commented Jul 14, 2024

@spacewander ready to review

assert.False(t, IsChanged(f))
time.Sleep(1000 * time.Millisecond)

tmpfile, _ := os.CreateTemp("./", "example")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tmpfile, _ := os.CreateTemp("./", "example")
tmpfile, _ := os.CreateTemp("", "example")

would be better so that the test case won't pollute the git repo.

plugins/pkg/file/fs.go Show resolved Hide resolved
for {
select {
case event := <-w.watcher.Events:
if _, exists := w.files[event.Name]; exists {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case doesn't work on Mac. I spent some time investigating it and found that the event.Name is example1104322652 and the map is map[./example1104322652:true]. Let's convert the input file name to absolute path.

@lyt122
Copy link
Contributor Author

lyt122 commented Jul 15, 2024

@spacewander

if _, exists := w.files[file.Name]; !exists {
w.files[file.Name] = true
}
dir, err := filepath.Abs(file.Name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a Dir call?

for {
select {
case event := <-w.watcher.Events:
if event.Op&fsnotify.Chmod == fsnotify.Chmod {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Op is Chmod | SomethingElse, the SomethingElse event will be ignored.

@lyt122
Copy link
Contributor Author

lyt122 commented Jul 15, 2024

@spacewander

for {
select {
case event := <-w.watcher.Events:
if event.Op.Has(fsnotify.Chmod) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this solve the issue that the Op both has Chmod and SomethingElse flags? In this case, the SomethingElse event will be dropped.

@lyt122
Copy link
Contributor Author

lyt122 commented Jul 16, 2024

@spacewander

if event.Op&fsnotify.Chmod != 0 {
event.Op &= ^fsnotify.Chmod // Remove the Chmod bit
if event.Op == 0 {
continue // Skip if it was only a Chmod event
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code works but why don't we just use event.Op == fsnotify.Chmod?

@spacewander spacewander merged commit f9cec7a into mosn:main Jul 17, 2024
13 checks passed
@spacewander
Copy link
Member

Merged. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use fsnotify to detect model/policy files change in casbin plugin
2 participants