close
Skip to content

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

Merged
imorland merged 4 commits into1.xfrom
im/perf
Feb 21, 2026
Merged

[1.x] perf: eliminate redundant DB writes in auth middleware and cache notification counts#4365
imorland merged 4 commits into1.xfrom
im/perf

Conversation

@imorland
Copy link
Copy Markdown
Member

@imorland imorland commented Feb 19, 2026

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
  • ListNotificationsController updates read_notifications_at via markNotificationsAsRead()

ApplicationInfoProvider::identifyDatabaseVersion()

Cache the DB version query for 24 hours. $this->cache (CacheRepository) is already injected into the class.


The 2.x port of this PR is #4380.

@imorland imorland added this to the 1.8.14 milestone Feb 19, 2026
@imorland imorland changed the title Im/perf [1.x] perf: assorted performance improvements Feb 19, 2026
imorland added a commit that referenced this pull request Feb 21, 2026
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 changed the title [1.x] perf: assorted performance improvements [1.x] perf: eliminate redundant DB writes in auth middleware and cache notification counts Feb 21, 2026
@imorland imorland marked this pull request as ready for review February 21, 2026 15:24
@imorland imorland requested a review from a team as a code owner February 21, 2026 15:24
imorland added a commit that referenced this pull request Feb 21, 2026
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 merged commit aa5fd73 into 1.x Feb 21, 2026
776 of 777 checks passed
@imorland imorland deleted the im/perf branch February 21, 2026 18:06
imorland added a commit that referenced this pull request Feb 26, 2026
…ession of #4365)

#4365 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: call resolve('cache.store')->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 #4365) (#4391)

#4365 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: call resolve('cache.store')->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