Repository navigation
Conversation
GetDimension read the dimensions map without holding the app mutex while addDimension wrote to it under the mutex. Concurrent layout handling of objects with cyclic dimensions could then crash with "fatal error: concurrent map read and map write". The session object getters (GetSheetList, GetFieldList, GetDimensionList, GetCurrentSelections, etc.) had the same pattern for their cached pointers. Read these under the mutex as well, and bind event handlers to the object created in the call instead of re-reading the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review feedback: locking each field access separately did not coordinate initialization. Two callers could both create a session object, and the replaced one could register its change event after the setter had already run deregistration, leaking the object and its event handler. The object was also published before its layout and properties were fetched, so a concurrent caller could get a partially initialized one. Add lazySessionObject, which runs the whole create, fetch and register sequence under a per-object lock and publishes the object only when it succeeds. Concurrent callers wait and share the same object; a failed initialization is not cached and is retried by the next caller. With no object ever replaced, the set* methods and their deregistration paths are removed. GetAggregatedSelectionFields returns an empty string when current selections have not been initialized instead of dereferencing nil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review feedback: When creating a session object succeeded but fetching its layout or properties failed, the object was not cached and the next caller created a new one, leaving the first in the engine. initSessionObject runs the steps after creation and destroys the session object if any fails. GetAggregatedSelectionFields read currentSelections.layout directly while UpdateLayout replaced it under the CurrentSelections mutex. CurrentSelections.Layout and Properties now read under the mutex, and aggregation uses a single layout snapshot. This race predates the PR. Tests inject failures in CreateSessionObject, GetLayout and GetEffectiveProperties and check destroy requests, and race selection layout updates against GetAggregatedSelectionFields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
For future reference: The same unlocked layout read exists in other senseobjects types: sheet list, dimension and field list iteration, bookmark list, the ODAG nav point and the dynamic app view list. Most carry the same //TODO DECISION comment. AppPropsList.UpdateLayout also reads its items without the lock while queued addItem calls write to it. All of these predate the PR and none were changed. I listed them under "Follow-ups" in the PR description rather than widening this PR; they'd fit better as a separate change. |
atluq
self-requested a review
October 28, 2026 08:23
atluq
approved these changes
Oct 28, 2026
DnlLrssn
reviewed
Oct 28, 2026
Review feedback: the client never destroys session objects that fail initialization, so the simulation should not either. Sending DestroySessionObject only on server-side failures would make the load differ from real clients exactly when the server is struggling. The objects are cleaned up when the session times out after disconnect. initSessionObject is removed and the getters run their requests directly again. The CurrentSelections layout locking is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DnlLrssn
approved these changes
Oct 28, 2026
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.
Problem
A load test crashed after ~27 minutes with:
GetDimensionreadapp.dimensionswithout holdingapp.mutex, whileaddDimensionwrote to it under the mutex. Object layouts are handled in parallel via the pending handler, andSetHyperCubecallsGetDimensionfor every cyclic (Grouping == "C") dimension. So two objects with uncached cyclic dimensions could hit the map at the same time.The cached session objects (
GetSheetList,GetBookmarkList,GetVariableList,GetAppsPropsList,GetStoryList,GetLoadModelList,GetDimensionList,GetFieldList,GetCurrentSelections) had two related problems. The cached locale info (GetLocaleInfo) had the first one:Fix
GetDimensionreads the cache throughgetDimensionFromMap, which holds the mutex (same pattern asgetBookmarkFromMap). Duplicate fetches of the same dimension are harmless, so no further coordination is needed there.lazySessionObject[T](senseobjects/lazysessionobject.go) runs the whole create → fetch layout/properties →RegisterEventsequence under a lock per object. The locale info uses it too. It publishes the object (viaatomic.Pointer) only after that sequence succeeds. Concurrent callers wait and get the same object. A failed setup isn't cached, so the next caller retries.set*methods and their deregistration paths are removed.GetCurrentSelectionswithrequestDatarefreshes an existing object through its registered event function outside the setup lock, as before.GetAggregatedSelectionFieldsreads current selections without waiting, and returns""if they haven't been set up yet, instead of dereferencing nil.CurrentSelections.Layout()andProperties()now read under the object's mutex, andGetAggregatedSelectionFieldsworks from one layout snapshot. Before, it readlayoutdirectly whileUpdateLayoutreplaced it under the mutex. That race predates this PR.The setup lock is held across
SendRequest. This is safe: change events triggered bySendRequestare dispatched in their own goroutines and not waited on, and no setup sequence calls the same getter again.Tests
senseobjects/app_test.goadds a minimal fake engine socket:TestAppConcurrentGetDimension: reproduces the reported race atapp.go:548under-raceagainst the original code.TestAppConcurrentGetDimensionList: 20 concurrent callers must cause exactly oneCreateSessionObject, all get the same object, and every object has its layout and properties. Fails against the first commit of this PR (duplicate setup).TestAppGetDimensionListRetryAfterError: injects a failure inCreateSessionObject,GetLayoutorGetEffectiveProperties. In each case the failed setup isn't cached, the next call succeeds, and noDestroySessionObjectrequest is sent.TestAppConcurrentAggregatedSelectionFields: runs current selections layout updates againstGetAggregatedSelectionFields. It fails under-racewithout theCurrentSelectionslocking.go test -race ./...passes,go test -race -count=10 ./senseobjects/passes, andgo vetandgolangci-lintare clean for the changed packages.Follow-ups (pre-existing, not changed here)
senseobjectstypes readlayoutwithout the mutex thatsetLayoutuses:SheetList(Layout(),GetSheetEntry),OdagToolbarNavpoint(Layout(),GetHypercubeData) andDynamicAppViewList(Layout()). TheLayout()accessors are marked//TODO DECISION: wait for write lock?.GetSheetEntryis reachable from object handling insession/objecthandling.go, concurrently with sheet list change events.AppPropsList.UpdateLayoutiteratesitemswithout the lock, while queuedaddItemcalls write to it.🤖 Generated with Claude Code