From 79e7f8825063bd166b7a738fa976838350349062 Mon Sep 17 00:00:00 2001 From: Joshua Wa Date: Mon, 3 Aug 2026 08:36:14 -0400 Subject: [PATCH 1/3] Throttle checkin cache key refresh when the key list is empty CheckinCache.UpdateKeys only honored its 10-second refresh interval when the cached key list was already non-empty: if ( currentKeys.Any() && ( now - _lastKeysRefreshUtc ) < KeysRefreshInterval ) An empty list is a legitimate result -- overnight, before the first check-in of the day, there are no matching Attendance rows -- but the guard treated it as a cold cache. Every caller therefore fell through to keyFactory() instead of serving from cache, and the throttle never applied. AttendanceCache.KeyFactory() emits a full scan of dbo.Attendance (~23,900 logical reads, ~1.8s CPU) to return zero rows; cost tracks index size, not result size. CheckinMonitor.BindTable calls into the cache once per open room on a 10-second render loop, so a single screen produced ~25,000 rebuilds an hour. Observed on rockprod as a sustained ~98% CPU plateau from 01:00 until the day's first check-in, on roughly 65% of nights. Dropping currentKeys.Any() makes the throttle depend only on when the list was last rebuilt. The throttled branch still applies ensureKey and removeKey, so a live check-in's key is injected immediately with no database round trip. Measured on RockDev: 150 rebuilds/minute before, 5.2/minute after -- one per ~11.5s against the 10s interval. Co-Authored-By: Claude Opus 5 (1M context) --- Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs b/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs index 01ca8fc17..148ea46ce 100644 --- a/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs +++ b/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs @@ -176,8 +176,11 @@ private static List UpdateKeys( Func> keyFactory, string en var now = DateTime.UtcNow; var currentKeys = AllKeys(); - // If within throttle window and we have existing keys - if ( currentKeys.Any() && ( now - _lastKeysRefreshUtc ) < KeysRefreshInterval ) + // If within the throttle window, serve the cached key list as-is. + // An empty list is a valid answer (e.g. overnight, before the day's first + // check-in) and must still be throttled -- otherwise every caller re-runs + // the key query, which is a full scan of Attendance returning no rows. + if ( ( now - _lastKeysRefreshUtc ) < KeysRefreshInterval ) { bool modified = false; From 0a7c9ab5e0ea2d2186851b48b4630151e9776645 Mon Sep 17 00:00:00 2001 From: jwakefield-secc Date: Wed, 5 Aug 2026 14:16:44 -0400 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs b/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs index 148ea46ce..72003e9f5 100644 --- a/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs +++ b/Plugins/org.secc.FamilyCheckin/Cache/CheckinCache.cs @@ -176,10 +176,10 @@ private static List UpdateKeys( Func> keyFactory, string en var now = DateTime.UtcNow; var currentKeys = AllKeys(); - // If within the throttle window, serve the cached key list as-is. - // An empty list is a valid answer (e.g. overnight, before the day's first - // check-in) and must still be throttled -- otherwise every caller re-runs - // the key query, which is a full scan of Attendance returning no rows. + // If within the throttle window, serve the cached key list (applying ensure/remove below). + // An empty list can be a valid answer (e.g. overnight, before the day's first check-in) and must + // still be throttled; otherwise every caller re-runs keyFactory(), which may be an expensive DB query + // (e.g., Attendance keyFactory can trigger a full Attendance scan that returns zero rows). if ( ( now - _lastKeysRefreshUtc ) < KeysRefreshInterval ) { bool modified = false; From 0e6fd0f26a9218af4480af5c49219479e275e4ec Mon Sep 17 00:00:00 2001 From: Joshua Wa Date: Wed, 5 Aug 2026 14:44:30 -0400 Subject: [PATCH 3/3] Address review: document CheckinCache key-list throttling in README Adds a Caching subsection to the org.secc.FamilyCheckin README covering the key-list layer and the 10-second refresh throttle from this PR, including the accepted tradeoff (AllKeys-derived views can read empty for up to 10s after a cache clear during active check-in). Requested by @stphnlee in the PR #281 review. Co-Authored-By: Claude Opus 5 (1M context) --- Plugins/org.secc.FamilyCheckin/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Plugins/org.secc.FamilyCheckin/README.md b/Plugins/org.secc.FamilyCheckin/README.md index f172872d9..cb2fc1de4 100644 --- a/Plugins/org.secc.FamilyCheckin/README.md +++ b/Plugins/org.secc.FamilyCheckin/README.md @@ -127,6 +127,23 @@ Quartz `IJob`s (both `[DisallowConcurrentExecution]`); scheduled in Rock, not se |------------|---------| | `CheckinGroupFieldType` | Picks a single or (configurably) multiple check-in groups; stores `Group.Guid`. Backed by `CheckinGroupPicker` and `CheckinGroupFieldAttribute`. | +### Caching + +`CheckinCache` — subclassed by `AttendanceCache`, `OccurrenceCache`, and +`MobileCheckinRecordCache` — keeps a per-type **key list** in `RockCache` (region `AllItems`, key +`{TypeName}:All`) next to the cached entities, so `All()` / `AllKeys()` can enumerate without a DB +round trip. Each subclass supplies a `keyFactory` that rebuilds that list from the database. +`CheckinKioskTypeCache` is **not** part of this layer — it uses Rock's own `ModelCache`. + +**Key-list refresh throttling (PR #281):** key-list refreshes are throttled to once per 10 seconds, +per cache type, per web-farm node — **including when the cached list is empty**. An empty list is a +valid state (overnight, before the day's first check-in), and refreshing on every call re-ran +`keyFactory()` every time; for `AttendanceCache` that is a full `dbo.Attendance` scan returning zero +rows. Tradeoff: after a cache clear or flush during active check-in, `AllKeys`-derived views +(attendance / occupancy) can read empty for up to 10 seconds before self-healing. Accepted — the +`ensureKey` / `removeKey` paths still apply inside the throttle window, so individual check-ins are +not lost. + ## Dependencies & Integrations - **Rock:** check-in engine (`CheckInState`, `CheckInActionComponent`), workflow engine,