Refresh check and cache reload¶
| Field | Value |
|---|---|
| Owner | CheckForUpdates (locking + reload orchestration) and TestForRefreshSettings/NeedsRefresh (the due-for-refresh probe) |
| Trigger | Every call to Load section (unconditionally, before checking whether the specific section is already loaded) |
| Inputs | Global cache state: Settings (empty or not), LastCheckForRefresh, LastRefreshed, TimeToWaitSeconds (60, set during Initialize environment client) |
| Outputs | true/false — whether a full cache reload was performed during this call |
Steps¶
CheckForUpdatesacquires_refreshSettingsLock(a plainlock, not theSectionsSemaphore) for the duration of the check.- It evaluates
NeedsRefresh, which callsTestForRefreshSettings: - If the global
Settingsdictionary is empty (nothing loaded yet, including on the very first call), it immediately returnstrue— no server probe is made in this case. - Otherwise, if
LastCheckForRefresh.AddSeconds(TimeToWaitSeconds) < DateTime.Now(i.e., at least 60 seconds have passed since the last probe) (BR-EC-14), it callsGET /settings/lastRefreshedon the Environment API, setsLastCheckForRefresh = DateTime.Now, and returnstrueonly if the call was successful and the returned server timestamp is later than the client's currentLastRefreshed. - Otherwise (probe not yet due) it returns
falsewithout any network call. - If
NeedsRefreshreturnedfalse,CheckForUpdatesreturnsfalseimmediately — no reload. - If
NeedsRefreshreturnedtrue,CheckForUpdatesacquiresSectionsSemaphore, then: setsLastRefreshed = DateTime.Nowbefore loading (not after a successful load completes), clears the entire globalSettingsdictionary, reloads the literal-empty-string "root" section (LoadSettings()with no section argument — this is distinct from the application's named track section; see Load section step 5), and reloads every section that was previously recorded inSections(iterating a snapshot array of theHashSet, callingLoadSettings(section)for each) — this reset is what allows each section to be loaded from the Environment API again, once, in the new cache generation (BR-EC-13). It then releases the semaphore and returnstrue. - Because
Settingsis cleared before the reload loop runs, and any one section's reload can throwEnvironmentClientException(see Load section step 6), a mid-reload failure can leave the global cache empty or only partially repopulated — the whole-cache reload is not atomic.LastRefreshedis always set to the start-of-reload timestamp before the loop runs (step 4), regardless of which branch of step 2 triggered the reload.LastCheckForRefresh, however, is only updated inside the server-probe branch of step 2 (TestForRefreshSettings'sLastCheckForRefresh = DateTime.Nowassignment) — the empty-Settingsbranch returnstrueimmediately with no probe and leavesLastCheckForRefreshunchanged. So after an empty-cache-triggered reload, the very next call's probe timing is governed by whateverLastCheckForRefreshalready held (likely its unset default), not by the reload's start time. This failure has no guaranteed automatic repair on the next call: onceLastRefreshedhas been advanced, a subsequent due probe only triggers another reload if the server'slastRefreshedtimestamp is itself later than this newly-advancedLastRefreshed— a due probe alone does not guarantee a reload. If a mid-reload failure leavesSettingscompletely empty, the next call bypasses the probe/throttle entirely (via the empty-cache check) and retries the reload immediately; ifSettingsis left non-empty, the next call is subject to the normal 60 s probe throttle and may returnfalse(no refresh) until a probe comes due and the server timestamp has newly advanced past the already-updatedLastRefreshed. Separately,Sectionsitself is never cleared by this reload (onlySettingsis) — if a named section's individualLoadSettings(section)call throws partway through the loop in step 4, that section remains recorded inSectionsfrom before the refresh even though its correspondingSettingsentries were just cleared and never restored; see Load section step 7 for the resulting loaded-but-empty, un-retried state. - Whether the
GET /settings/lastRefreshedcall's body actually deserializes as aDateTimeand how a malformed/missing body is handled is determined by the injectedIHttpJsonClient/IHttpRequestResultimplementation, not by this library** .
Process flow¶
flowchart TD
A["CheckForUpdates (every LoadSection call)"] --> B[Acquire _refreshSettingsLock]
B --> C{Settings dictionary empty?}
C -- Yes --> D["NeedsRefresh = true (no probe)"]
C -- No --> E{"60s since LastCheckForRefresh?"}
E -- No --> F["NeedsRefresh = false"]
E -- Yes --> G["GET /settings/lastRefreshed"]
G --> H["LastCheckForRefresh = now"]
H --> I{"Successful and server time > LastRefreshed?"}
I -- Yes --> D
I -- No --> F
F --> J["Return false: no reload"]
D --> K[Acquire SectionsSemaphore]
K --> L["LastRefreshed = now (set before loading)"]
L --> M[Settings.Clear]
M --> N["LoadSettings() for root section (literal empty string)"]
N --> O["LoadSettings(section) for each known Sections entry"]
O --> P["Release semaphore, return true"]
Note: Statements marked with
*were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts. Statements marked with**could not be confirmed; treat as unknown until verified.
Sources¶
src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs