-
Notifications
You must be signed in to change notification settings - Fork 4
Add locked accessors for the fields Refresh() replaces #94
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
Merged
+249
−53
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| package securityspy_test | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "sync" | ||
| "sync/atomic" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestRefreshConcurrentReaders is a race-detector test. Refresh() replaces | ||
| // Cameras, Groups and Info, and apps commonly call it from a background retry | ||
| // loop or an event handler while requests read those same fields. The Get | ||
| // accessors are the only safe way to read them, so the readers below run for | ||
| // as long as the refresher does. | ||
| // | ||
| // The workers report with t.Errorf rather than require: FailNow may only be | ||
| // called from the goroutine running the test. | ||
| func TestRefreshConcurrentReaders(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| serverObj, _, _ := testServerWithCamera(t) | ||
| done := make(chan struct{}) | ||
|
|
||
| var wait sync.WaitGroup | ||
|
|
||
| wait.Go(func() { | ||
| defer close(done) | ||
|
|
||
| for range 25 { | ||
| err := serverObj.Refresh() | ||
| if err != nil { | ||
| t.Errorf("Refresh: %v", err) | ||
|
|
||
| return | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| wait.Go(func() { | ||
| for !isDone(done) { | ||
| cams := serverObj.GetCameras() | ||
| if cams == nil { | ||
| t.Error("GetCameras returned nil during a refresh") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| if cams.ByNum(3) == nil { | ||
| t.Error("camera 3 went missing during a refresh") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| if len(cams.All()) == 0 { | ||
| t.Error("camera list emptied during a refresh") | ||
|
|
||
| return | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| wait.Go(func() { | ||
| for !isDone(done) { | ||
| info := serverObj.GetInfo() | ||
| if info == nil { | ||
| t.Error("GetInfo returned nil during a refresh") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| _ = info.Version | ||
| _ = serverObj.GetGroups() | ||
| } | ||
| }) | ||
|
|
||
| wait.Wait() | ||
| } | ||
|
|
||
| // TestRefreshDoesNotBlockReaders: a refresh holds the write lock only for the | ||
| // swap, so a slow (or hung) systemInfo request must not stall readers. The | ||
| // handler parks the second refresh mid-request; if the refresh held the lock | ||
| // across the round trip, the reads below would block until the test timed out. | ||
| func TestRefreshDoesNotBlockReaders(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| var ( | ||
| requests atomic.Int64 | ||
| parked = make(chan struct{}) | ||
| release = make(chan struct{}) | ||
| ) | ||
|
|
||
| serverObj := newTestServer(t, func(resp http.ResponseWriter, req *http.Request) { | ||
| if req.URL.Path != systemInfoPath { | ||
| http.NotFound(resp, req) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| if requests.Add(1) > 1 { // let the first refresh load the snapshot | ||
| close(parked) | ||
| <-release | ||
| } | ||
|
|
||
| resp.Header().Set("Content-Type", "application/xml") | ||
| _, _ = resp.Write([]byte(testSystemInfoV6)) | ||
| }) | ||
|
|
||
| require.NoError(t, serverObj.Refresh()) | ||
|
|
||
| refreshed := make(chan error, 1) | ||
| go func() { refreshed <- serverObj.Refresh() }() | ||
|
|
||
| <-parked | ||
|
|
||
| // The refresh is parked mid-request; reads still come from the old snapshot. | ||
| cams := serverObj.GetCameras() | ||
| if cams == nil || cams.ByNum(3) == nil { | ||
| t.Error("readers were blocked by an in-flight refresh") | ||
| } | ||
|
|
||
| if serverObj.GetInfo() == nil { | ||
| t.Error("GetInfo was blocked by an in-flight refresh") | ||
| } | ||
|
|
||
| close(release) | ||
| require.NoError(t, <-refreshed) | ||
| } | ||
|
|
||
| func isDone(done <-chan struct{}) bool { | ||
| select { | ||
| case <-done: | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, fixed in a3ee38f — this would have been a real regression.
RefreshContextnow builds the info, camera list and groups into locals (camera wiring moved towireCameras) and holdsmuonly for the three assignments; a newrefreshMukeeps refreshes serialized with each other as before. AddedTestRefreshDoesNotBlockReaders, which parks a refresh mid-request and asserts reads still come from the previous snapshot. It fails against the old locking exactly as you described: the reader blocks until the client timeout fires,--- FAIL ... (10.01s) ... context deadline exceeded.