close
Skip to content

[2.x] perf: eliminate redundant DB writes in auth middleware and cache notification counts#4380

Merged
imorland merged 1 commit into2.xfrom
perf/assorted-improvements
Feb 21, 2026
Merged

[2.x] perf: eliminate redundant DB writes in auth middleware and cache notification counts#4380
imorland merged 1 commit into2.xfrom
perf/assorted-improvements

Conversation

@imorland
Copy link
Copy Markdown
Member

Ports the performance improvements from #4365 (1.x) to 2.x, adapting for API changes in 2.x.

Changes

AccessToken::touch()

Only update last_ip_address / last_user_agent when the values have actually changed, and only call save() when the model is dirty. On high-traffic sites this eliminates a DB write on every token touch when last_activity_at has not yet elapsed the update threshold.

AuthenticateWithHeader / AuthenticateWithSession

Guard $actor->save() with isDirty() after updateLastSeen(). Since updateLastSeen() is throttled to 180 seconds, requests within that window were previously triggering an unconditional DB write even when nothing changed.

Notification count caching

Cache getUnreadNotificationCount() and getNewNotificationCount() for 5 minutes with active invalidation on all write paths. The TTL is a safety net; the cache is eagerly invalidated whenever:

  • Notification::notify() creates new notifications for recipients
  • ReadNotificationHandler marks a notification read
  • ReadAllNotificationsHandler marks all notifications read
  • NotificationResource (index) updates read_notifications_at via markNotificationsAsRead()

For ReadAllNotificationsHandler and ReadNotificationHandler, CacheRepository is injected via the constructor for testability rather than using resolve().

ApplicationInfoProvider::identifyDatabaseVersion()

Cache the DB version query for 24 hours. $this->cache (CacheRepository) is already injected into the class. In 2.x the query uses a match expression for SQLite vs MySQL/MariaDB/PostgreSQL, which is preserved inside the closure.

2.x adaptations

  • ListNotificationsController no longer exists; its markNotificationsAsRead() call is now in NotificationResource's Index endpoint before() hook — cache invalidation added there
  • identifyDatabaseVersion() in 2.x uses a match expression (handles SQLite separately); both branches are wrapped in the cache closure

Ports the performance improvements from #4365 (1.x) to 2.x, adapting
where the API has changed.

**AccessToken::touch()**
- Only update last_ip_address / last_user_agent when the values have
  actually changed, avoiding unnecessary dirty-marking
- Only call save() when the model is dirty, eliminating a DB write on
  every token touch when activity was throttled

**AuthenticateWithHeader / AuthenticateWithSession**
- Guard $actor->save() with isDirty() after updateLastSeen()
- updateLastSeen() is throttled to 180 seconds; on requests within that
  window the actor was already being saved unconditionally

**User notification count caching**
- Cache getUnreadNotificationCount() and getNewNotificationCount() for
  5 minutes with active invalidation on all write paths
- The TTL is a safety net; the cache is eagerly invalidated whenever
  notifications are created, read, or the user views notifications

**Cache invalidation write paths**
- Notification::notify() — invalidates both count keys for all recipients
- ReadNotificationHandler — invalidates both count keys
- ReadAllNotificationsHandler — invalidates both count keys
- NotificationResource (Index endpoint) — invalidates new_notification_count
  after markNotificationsAsRead() updates read_notifications_at

**ApplicationInfoProvider::identifyDatabaseVersion()**
- Cache the DB version query for 24 hours; $cache is already injected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@imorland imorland requested a review from a team as a code owner February 21, 2026 15:21
@imorland imorland added this to the 2.0.0-beta.7 milestone Feb 21, 2026
@imorland imorland changed the title [2.x] perf: assorted performance improvements [2.x] perf: eliminate redundant DB writes in auth middleware and cache notification counts Feb 21, 2026
@imorland imorland merged commit 15da85d into 2.x Feb 21, 2026
35 checks passed
@imorland imorland deleted the perf/assorted-improvements branch February 21, 2026 15:29
imorland added a commit that referenced this pull request Feb 26, 2026
…ession of #4380)

#4380 added caching to getUnreadNotificationCount() and getNewNotificationCount()
and correctly invalidated the cache in ReadAllNotificationsHandler and
ReadNotificationHandler. DeleteAllNotificationsHandler was missed, so deleting
all notifications left the stale count in the cache for up to 5 minutes —
causing the notification badge to reappear after deletion until the cache
naturally expired.

Fix: inject CacheRepository and call forget() on both count keys after deletion,
matching the pattern already used in ReadAllNotificationsHandler.

Also adds a regression test that primes the cache with a stale count, calls
DELETE /notifications, and asserts both cache keys are cleared.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
imorland added a commit that referenced this pull request Feb 26, 2026
…ession of #4380) (#4390)

#4380 added caching to getUnreadNotificationCount() and getNewNotificationCount()
and correctly invalidated the cache in ReadAllNotificationsHandler and
ReadNotificationHandler. DeleteAllNotificationsHandler was missed, so deleting
all notifications left the stale count in the cache for up to 5 minutes —
causing the notification badge to reappear after deletion until the cache
naturally expired.

Fix: inject CacheRepository and call forget() on both count keys after deletion,
matching the pattern already used in ReadAllNotificationsHandler.

Also adds a regression test that primes the cache with a stale count, calls
DELETE /notifications, and asserts both cache keys are cleared.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant