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>
The convert_gids_to_sgids method resolved GIDs globally and minted
valid SGIDs without verifying the record belongs to the importing
account. This adds the same account_id check that the export path
already has, plus RecordNotFound handling for invalid GIDs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename histogram from :request_wait_seconds to :request_wait so Yabeda's
unit: :seconds produces gvl_request_wait_seconds instead of
gvl_request_wait_seconds_seconds.
* Add GVL contention metrics via gvltools and Yabeda
Expose per-request and process-wide GVL wait time as Prometheus metrics
to diagnose suspected GVL contention from Solid Cable and Action Cable
threads in Puma workers.
Metrics: gvl_request_wait_seconds (histogram), gvl_waiting_threads
(gauge), gvl_global_timer_total_seconds (gauge).
* Add unit: :seconds to GVL histogram for consistency
* Remove unused marked JS dependency
* Remove unused redcarpet dependency
* Render inline code in card titles
Add card_html_title helper that HTML-escapes input then converts
backtick-wrapped text to <code> elements. Apply to card titles in
board preview, card detail, public views, and notification emails.
Style inline code elements in titles to match description styling.
Co-authored-by: Andy Smith <andy@37signals.com>
---------
Co-authored-by: Andy Smith <andy@37signals.com>
So you can do something like this:
```ruby
require "stackprof"
report = StackProf.run(mode: :wall, interval: 100) do
Account.find_by(external_account_id: 123)
end
StackProf::Report.new(report).print_text
```
As we now update notifications with unread counts we sort them by the
time they were updated instead of when they were created. To keep that
performant we also have to change the previous indices that used
created_at to use updated_at.
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.
- Bump Lexxy's version to latest 0.7.4
- Enable tables with the recent UX enhancements
- Use Lexxy's built-in CSS for rich text rendering, removing the duplicate CSS from Fizzy