Status: IN_PROGRESS
Priority: HIGH
Area: Economy / Insurance
Problem
InsurancePriceRefreshService.Refresh() opens a TransactionScope with a using declaration, calls scope.Complete(), and then calls InsuranceHelper.LoadInsurancePrices() while still inside the scope's lifetime (src/Perpetuum/Services/Insurance/InsurancePriceRefreshService.cs:49-52 at f9ddac2):
using var scope = Db.CreateTransaction();
_ = Db.Query().CommandText("exec usp_RecalculateInsurancePrices").Timeout(120).ExecuteNonQuery();
scope.Complete();
InsuranceHelper.LoadInsurancePrices();
A using declaration disposes at the end of the method, not at Complete(). Complete() only casts the commit vote — the scope stays the ambient transaction until Dispose(). LoadInsurancePrices() therefore issues its query with Transaction.Current pointing at a scope that is already complete, and DbQuery.ExecuteHelper calls connection.Open() (src/Perpetuum/Data/DbQuery.cs:55), which reads Transaction.Current for transacted connection pooling and throws:
System.InvalidOperationException: The current TransactionScope is already complete.
at System.Transactions.Transaction.get_Current()
at Microsoft.Data.ProviderBase.DbConnectionPool.GetFromTransactedPool(Transaction& transaction)
at Microsoft.Data.SqlClient.SqlConnection.Open(SqlConnectionOverrides overrides)
at Perpetuum.Data.DbQuery.ExecuteHelper[T](Func`2 execute) in DbQuery.cs:line 55
at Perpetuum.Services.Insurance.InsuranceHelper.LoadInsurancePrices() in InsuranceHelper.cs:line 447
at Perpetuum.Services.Insurance.InsurancePriceRefreshService.Refresh() in InsurancePriceRefreshService.cs:line 52
The exception propagates to the catch in RefreshAsync, so every run — the startup run and each daily run — is counted as a failure and logs refresh failed (N consecutive failure(s)). The success line at InsurancePriceRefreshService.cs:53 never runs.
Impact
_insurancePrices (src/Perpetuum/Services/Insurance/InsuranceHelper.cs:401) is a static cache populated lazily: on a miss, GetInsurancePrice reads dbo.insuranceprices once 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 the ProductionSetInsurance request handler.
With Refresh() throwing before that call, the daily recalculation updates dbo.insuranceprices but the running server keeps quoting the values it cached earlier. Fees and payouts drift from the recalculated table for as long as the process stays up, and a restart is the only thing that clears it. This is the same player-visible symptom ISSUE-036 reports, so applying that issue's migration alone does not restore correct prices on a long-running server.
The MERGE itself is not lost: Complete() has already been called when the exception unwinds, so Dispose() still commits.
Cross-Reference — ISSUE-036 (IN_PROGRESS)
This 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(). Applying docs/db_structure/migrations/ISSUE-036-fix-insurance-proc-self-dependency.sql to a local P36.8 database removed the nesting level exceeded exception from the startup log and exposed this one in its place, at the next line of the same method.
ISSUE-036's production verification list should gain one step: after applying the migration, confirm the log carries InsurancePriceRefreshService: prices recalculated and cache reloaded. rather than another refresh failed line.
Fix
src/Perpetuum/Services/Insurance/InsurancePriceRefreshService.cs — replace the using declaration with a using block that closes immediately after scope.Complete(), so the transaction 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, which is what the original code already intended.
Notes
Reproduced on a local P36.8 database (develop at f9ddac2) with the ISSUE-036 migration applied. Status is IN_PROGRESS rather than DONE because production still has the ISSUE-036 migration pending — until it is applied there, the SQL error masks this code path and the fix cannot be observed live.
Imported from docs/backlog/issues.md in OpenPerpetuum/PerpetuumServer2.
Status: IN_PROGRESS
Priority: HIGH
Area: Economy / Insurance
Problem
InsurancePriceRefreshService.Refresh()opens aTransactionScopewith ausingdeclaration, callsscope.Complete(), and then callsInsuranceHelper.LoadInsurancePrices()while still inside the scope's lifetime (src/Perpetuum/Services/Insurance/InsurancePriceRefreshService.cs:49-52atf9ddac2):A
usingdeclaration disposes at the end of the method, not atComplete().Complete()only casts the commit vote — the scope stays the ambient transaction untilDispose().LoadInsurancePrices()therefore issues its query withTransaction.Currentpointing at a scope that is already complete, andDbQuery.ExecuteHelpercallsconnection.Open()(src/Perpetuum/Data/DbQuery.cs:55), which readsTransaction.Currentfor transacted connection pooling and throws:The exception propagates to the
catchinRefreshAsync, so every run — the startup run and each daily run — is counted as a failure and logsrefresh failed (N consecutive failure(s)). The success line atInsurancePriceRefreshService.cs:53never runs.Impact
_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.With
Refresh()throwing before that call, the daily recalculation updatesdbo.insurancepricesbut the running server keeps quoting the values it cached earlier. Fees and payouts drift from the recalculated table for as long as the process stays up, and a restart is the only thing that clears it. This is the same player-visible symptom ISSUE-036 reports, so applying that issue's migration alone does not restore correct prices on a long-running server.The
MERGEitself is not lost:Complete()has already been called when the exception unwinds, soDispose()still commits.Cross-Reference — ISSUE-036 (IN_PROGRESS)
This defect sat behind 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.ISSUE-036's production verification list should gain one step: after applying the migration, confirm the log carries
InsurancePriceRefreshService: prices recalculated and cache reloaded.rather than anotherrefresh failedline.Fix
src/Perpetuum/Services/Insurance/InsurancePriceRefreshService.cs— replace theusingdeclaration with ausingblock that closes immediately afterscope.Complete(), so the transaction is disposed andTransaction.Currentis null again beforeLoadInsurancePrices()runs. This matches theusing (var scope = Db.CreateTransaction())form used throughoutPerpetuum.RequestHandlers. No logic, no SQL and no transaction boundary changes: theEXECremains the only statement inside the transaction, which is what the original code already intended.Notes
Reproduced on a local P36.8 database (
developatf9ddac2) with the ISSUE-036 migration applied. Status isIN_PROGRESSrather thanDONEbecause production still has the ISSUE-036 migration pending — until it is applied there, the SQL error masks this code path and the fix cannot be observed live.Imported from
docs/backlog/issues.mdin OpenPerpetuum/PerpetuumServer2.