When concurrent NotifyRecipientsJobs process an Event and a Mention for
the same user+card, Rails' dirty tracking can skip writing source_type
in the UPDATE if it hasn't changed from the stale in-memory value,
leaving source_type and source_id mismatched (e.g. source_type='Event'
with a Mention's source_id, resulting in a nil source).
Force source_type to always be included in the UPDATE via
source_type_will_change! to prevent this.
Here's a sample timeline of this race condition happening in the real
world (with simplified IDs):
Two `NotifyRecipientsJob` involving notification `03fklpu`, same card,
same user, same comment — enqueued within 50ms of each other:
1. Both jobs load notification `03fklpu` — it has source_type='Mention'
(from a previous job)
2. `EventNotifier` writes at 21:22:36.051: ```sql SET
source_type='Event', source_id=<event_id>, unread_count=1 ```
3. `source_type` included because it changed ('Mention' → 'Event')
4. `MentionNotifier` writes at 21:22:36.057 (~6ms later): ```sql SET
source_id=<mention_id>, unread_count=1 ```
5. No `source_type`! It was `'Mention'` when loaded and `'Mention'` is
what it's setting → not dirty → skipped
6. Final DB state: `source_type='Event'` (from step 2, untouched),
`source_id=<mention_id>` (from step 3)
`Notification.source` now does `Event.find(<mention_id>) → nil.`
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
We had client-side notification stacking in the tray since launch, but now we want to stack notifications in the notifications page, in API responses and in email bundles.
We've seen some cases of deadlock when processing notifications, because
locks are gathered on the users in different orders. Let's try sticking
to a consistent order instead, which should cause the jobs to serialize
rather than deadlock.
Is confusing since "source" already captures the origin of the notifications. It's
needed to interpret things at rendering time, so we can query things as needed there.
Instead of delegating. With a polymorphic relationship, relying on a certain
attribute implies doing things like aliasing "mentioner" to "creator" or similar.
This provides a way to set the level of involvement that a user has with
a collection, and from which we determine the level of notifications to
send. Users can be access-only, watching, or being notified about
everything.
If you're access-only, you won't get an notifications. If you're
watching, you'll only get notifications for the items you're watching
(which includes the items you've been assigned, have commented on, etc).
If you're set to everything you'll get notifications about all activity
in that collection.
This change replaces our previous concept of subscriptions. Where
previously you'd subscribe to a collection to get notifications in it,
now you'll simply set the notification level on your access.