ISSUE-039: reload insurance price cache outside the completed TransactionScope - #23
Merged
Sellafield merged 1 commit intoAug 13, 2026
Conversation
InsurancePriceRefreshService.Refresh opened a TransactionScope with a using declaration, called scope.Complete(), then called InsuranceHelper.LoadInsurancePrices() while still inside the scope's lifetime. A using declaration disposes at the end of the method, and Complete() only casts the commit vote, so Transaction.Current still pointed at a completed scope when DbQuery.ExecuteHelper opened its connection for the reload. Opening a connection reads Transaction.Current for transacted pooling, which threw InvalidOperationException: The current TransactionScope is already complete. Every refresh therefore failed after the MERGE had already been voted for commit, so the static _insurancePrices cache was never reloaded. Since GetInsurancePrice only reads the database on a cache miss, a running server kept quoting the fees and payouts it had cached earlier, for the rest of the process lifetime. Wrap the transaction in a using block that closes immediately after Complete(), so the scope is disposed and Transaction.Current is null again before LoadInsurancePrices() runs. This matches the using (var scope = Db.CreateTransaction()) form used throughout Perpetuum.RequestHandlers. No logic, no SQL and no transaction boundary changes: the EXEC remains the only statement inside the transaction. The defect sat behind ISSUE-036. Until usp_RecalculateInsurancePrices was fixed, the ExecuteNonQuery on the previous line always threw error 217, so execution never reached LoadInsurancePrices(). Records the finding as ISSUE-039. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Adds
ISSUE-039and fixes it:InsurancePriceRefreshService.Refresh()never reloads the insurance price cache, because it callsInsuranceHelper.LoadInsurancePrices()while a completedTransactionScopeis still the ambient transaction.This defect sat behind ISSUE-036 and only became visible once that issue's migration was applied.
What happens
Refresh()atdevelop(f9ddac2):A
usingdeclaration disposes at the end of the method, not atComplete().Complete()only casts the commit vote — the scope remainsTransaction.CurrentuntilDispose().LoadInsurancePrices()therefore runs inside a scope that is already complete, andDbQuery.ExecuteHelpercallsconnection.Open()(src/Perpetuum/Data/DbQuery.cs:55), which readsTransaction.Currentfor transacted connection pooling:The exception reaches the
catchinRefreshAsync, so the startup run and every daily run is counted as a failure and logsrefresh failed (N consecutive failure(s)). TheMERGEitself is not lost —Complete()has already been called, soDispose()still commits — but the cache reload never happens.Why it matters
_insurancePrices(src/Perpetuum/Services/Insurance/InsuranceHelper.cs:401) is a static cache populated lazily: on a miss,GetInsurancePricereadsdbo.insurancepricesonce and keeps that value for the definition for the rest of the process lifetime.LoadInsurancePrices()is the only path that refreshes an already-cached definition during normal operation — the sole other caller is theProductionSetInsurancerequest handler.So the daily recalculation updates the table while the running server keeps quoting what it cached earlier, and only a restart clears it. That is the same player-visible symptom ISSUE-036 describes, which means applying the ISSUE-036 migration alone does not restore correct prices on a long-running server.
Relationship to ISSUE-036
Until
usp_RecalculateInsurancePriceswas fixed, theExecuteNonQueryon the previous line always threw error 217, so execution never reachedLoadInsurancePrices(). Applyingdocs/db_structure/migrations/ISSUE-036-fix-insurance-proc-self-dependency.sqlto a local P36.8 database removed thenesting level exceededexception from the startup log and exposed this one in its place, at the next line of the same method.Suggestion for ISSUE-036's production verification list: after applying the migration, also confirm the log carries
InsurancePriceRefreshService: prices recalculated and cache reloaded.rather than anotherrefresh failedline. Without that step ISSUE-036 can look resolved at the database level while the server still serves stale prices.The change
Replace the
usingdeclaration with ausingblock that closes immediately afterscope.Complete(), so the scope is disposed andTransaction.Currentis null again beforeLoadInsurancePrices()runs.This is the
using (var scope = Db.CreateTransaction())form already used throughoutPerpetuum.RequestHandlers(~25 call sites), so it introduces no new pattern. No logic, no SQL and no transaction boundary changes: theEXECremains the only statement inside the transaction, which is what the original code already intended.ISSUE-039is filed asIN_PROGRESSrather thanDONE, matching how ISSUE-036 is tracked: production still has the ISSUE-036 migration pending, and until it is applied there the SQL error masks this code path.Validation
Local P36.8 database,
developatf9ddac2, full server start and graceful shutdown for each run:refresh failedprices recalculated and cache reloadedThe run with the fix reached
[Online]in 41 s, spawned 3042 flocks — the same count as the run before it — and shut down gracefully to[Off]. Its startup log contains no exception of any kind. Database side after the run:sys.sql_expression_dependenciesfor the procedure returns exactly the 4 legitimate dependencies, anddbo.insurancepricesholds 67 rows with non-zerofeeandpayout.Build:
dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64— 0 errors. The 30 warnings are pre-existingPerpetuum.AdminTool(MVVMTK0034) and WiX warnings; none reference the changed file.Regression surface
Refresh()is the only method touched. The transaction now disposes one statement earlier than before, which is the intended boundary —LoadInsurancePrices()is a read that was never meant to participate in the recalculation transaction, and it does not write. Threading is unchanged:Refresh()still runs on theTask.Runstarted byRefreshAsync, guarded by the same_refreshingflag.🤖 Generated with Claude Code