Repository navigation
Conversation
…ounts as 2^48, so neither a stray Reload nor a recurring double dispose brings back the old failures Follow-up to #483, from review: - Reload after Dispose (a SIGHUP during shutdown, say) swapped in a new snapshot and dropped the live reference a second time: StaticAssets served again, and the snapshot a lease still held was never freed. Dispose now swaps the snapshot out under the reload lock; Reload does nothing once it is gone, Acquire throws, and a second Dispose finds nothing to drop. A 32-bit count used to wrap on the fourth Dispose and hand out leases on the freed snapshot. - The live reference counted as 2^30, so a double dispose on every request still freed the live snapshot after 2^30 of them (about 18 minutes at a million a second), under leases still out, and 2^30 leaked leases overflowed the count. As a long counting 2^48, either takes years. - Acquire throws as soon as the assets are disposed, even while leases are out; those keep their snapshot open until they are released. - Docs and the XML comments say "with each lease disposed once" where the guarantee needs it. - Tests: Reload after Dispose with a lease out; one to six Disposes; Acquire racing 5,000 Reloads, which fails if the refusal check throws unconditionally.
… a Dispose that is not idempotent The test disposed one to six times and checked that Acquire threw, which a long count with a Dispose that still subtracted per call passed too. It now holds a lease across the calls and checks that releasing it frees the snapshot; main and that mutation both keep the descriptor open after two calls. Also from review: Acquire's summary and the files guide add "each lease disposed once" to the promise about closed descriptors, and the comments this change had made untrue are fixed and cut to one line.
…r throw, and the comments and docs say what the count can do A double disposed lease is the case that drains the live reference, not a second Dispose of the assets, which Dispose no longer repeats.
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Follow-up to #483, from a review of it after merge. Both problems below are reproduced on main 58620b5.
What was wrong
Reload after Dispose brought the assets back, and leaked the old snapshot
StaticAssets.Disposeran outside the reload lock and left_cachepointing at the snapshot it dropped, andReloadnever checked for disposal.Acquirehanded out leases again, and the snapshot a request's lease still held was never freed.Dispose,Reload, release the lease. The old descriptor stays open, andAcquirereturns a lease.Disposewas not idempotent either. Each call subtracted the live reference again, the 32-bit count wrapped on the fourth call, andAcquirethen leased the freed snapshot.The 2^30 cushion ran out
Acquirethrew on assets nobody disposed.Acquirethrows.Acquirerefuses. Main before file: a lease disposed twice no longer frees the live snapshot, and Acquire after Dispose throws instead of spinning (#481) #483 took 2^32.The fix
Disposeswaps the snapshot out under the reload lock (_cachebecomes null) and drops its live reference once.Disposefinds nothing to drop.Reloaddoes nothing once the assets are disposed.AcquirethrowsObjectDisposedException. Its null check takes the place of the null check the JIT already did.long, and the live reference counts as 2^48. A double dispose on every request, or leaked leases, would take years at a million a second to reach either end. TheInterlockedoperations are the same single instructions on x64 and arm64.Acquirekeeps file: a lease disposed twice no longer frees the live snapshot, and Acquire after Dispose throws instead of spinning (#481) #483's check, and a refusal from the snapshot that is still live still throws rather than spins.Behaviour
Acquirethrows as soon as the assets are disposed, even while leases are out. Before, it kept leasing the dropped snapshot until those leases drained. The leases already out keep their snapshot open until they are released.ReloadafterDisposedoes nothing, where before it revived the assets.CountandRootDirthrowObjectDisposedExceptionafterDispose. Before, they returned the dropped snapshot's values.Disposecan be called any number of times.API
None. Behaviour only, as above.
Tests
New in the File suite (
LeaseTests.cs):file: after Dispose, Reload does nothing and Acquire throws, and a lease still out keeps its snapshotfile: Dispose can be called any number of timesAcquirehas to throw.file: Acquire racing Reload always gets a lease on a whole snapshotAcquirecalls threw.file: Dispose racing Reload leaves no descriptor open and nothing to leaseAcquirehas to throw, and no descriptor may stay open on the file.Disposeoutside the reload lock, the reload throwsNullReferenceExceptionin the first round.file: after Dispose, Reload does nothing...also checks thatCountandRootDirthrow.Against main's
ioxide.file(the tests only):With the fix:
The 2^48 cushion has no test, since it would take 2^48 iterations to reach. The repro above (2^30 + 1 double disposes) shows the old limit.
Bench
None.
Acquire,TryAddRefandReleaserun the same instructions on along. The only addition is a null check that replaces the JIT's own.