Draft: Stop writing the label event reference on a read request
This is the change that closes INC-12449. It removes save if changed? from ResourceLabelEvent#refresh_invalid_reference, so rendering a label note no longer writes resource_label_events.reference. The in-memory derive stays, so a row the backfill has not reached yet still renders the correct reference; it just does not persist it anymore.
Honest caveat: LabelNote#update_outdated_reference still calls this once per event, so a pre-backfill row still costs one LabelsFinder query per event per render, the same query count as today minus the write, except now it happens on every render instead of once. MR 3 and MR 6 both shrink that population toward zero: MR 3 stops new rows from ever needing it, MR 6 backfills the rows that predate MR 3. Removing the derive-on-read path entirely is a deliberate 19.6 follow-up, once the backfill has had time to run against gitlab.com.
This has to merge after MR 3 and MR 6. Removing the write before the entry points stored the reference would mean every render derives it again forever with no way to shrink that cost. MR 6 is what empties the population of rows that still need deriving.
Part of the stack for #628135 (INC-12449), targeting 628135-06-backfill-reference. Merge in this order. The numbers follow the branch names, so there is no MR 2: that change was folded into MR 3 and !255083 (closed) is closed.
- MR 1 (!255082 (merged)): Render group level work item label notes against the group - merged 2026-09-21
- MR 3 (!255091 (merged)): Store the label event reference when the event is written - merged 2026-10-07
- MR 4 (!255092): Delete label events when their label is deleted
- MR 4b (!255238): Record deleted label events in the label audit event
- MR 5 (!255093): Link a project label in a group context to its project
- MR 6 (!255096): Backfill the reference on existing label events
- MR 7 (!255097): Stop writing the label event reference on a read request <-- this MR
How to reproduce and check by hand
This is the incident. Rendering a label note used to issue an UPDATE against resource_label_events, so a GET to the notes endpoint wrote to the database.
In the UI:
- Open an issue with label activity whose
referencehas not been backfilled. - Watch
log/development.logwhile the page loads. - Look for an
UPDATE "resource_label_events"during the GET.
In a rails console (gdk rails console), wrapped in a transaction that rolls back, so it leaves nothing behind:
ActiveRecord::Base.transaction do
user = User.find_by_username('root')
org = user.organizations.first
suffix = SecureRandom.hex(4)
group = Groups::CreateService.new(user, name: "repro-#{suffix}", path: "repro-#{suffix}",
organization_id: org.id).execute[:group]
group.add_owner(user)
project = Projects::CreateService.new(user, name: "proj-#{suffix}", path: "proj-#{suffix}",
namespace_id: group.id, organization_id: org.id).execute
issue = project.issues.create!(author: user, title: 'repro 628135')
label = Labels::CreateService.new(user, title: 'bug').execute(project: project)
ResourceEvents::ChangeLabelsService.new(issue, user).execute(added_labels: [label])
event = issue.resource_label_events.last
event.update_column(:reference, nil) # a row from before the backfill
puts "reference before render: #{event.reload.reference.inspect}"
updates = 0
sub = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, payload|
updates += 1 if payload[:sql] =~ /UPDATE "resource_label_events"/
end
rendered = ResourceEvents::SyntheticLabelNotesBuilderService.new(issue.reset, user).execute.map(&:note)
ActiveSupport::Notifications.unsubscribe(sub)
puts "rendered note: #{rendered.join(' | ')}"
puts "UPDATE statements: #{updates}"
puts "reference after render: #{event.reload.reference.inspect}"
raise ActiveRecord::Rollback
endObserved output
The script above was run on the parent branch and on this branch; what follows is the actual output from both runs.
Before, on 628135-06-backfill-reference:
reference before render: nil
rendered note: added ~35 label
UPDATE statements: 1
reference after render: "~35"After, on 628135-07-remove-write-on-get:
reference before render: nil
rendered note: added ~36 label
UPDATE statements: 0
reference after render: nilThe note still renders correctly with a null stored reference, so removing the write costs nothing at read time. This is safe only once MR 3 (write path) and MR 6 (backfill) are both in, which is why this MR sits last in the chain.
Both runs were done on a disposable GDK box whose GDK was at master 8d8af6729f, with each branch applied over it as a runtime-only patch and migrations run for real where the branch adds them.
Testing
Six new examples across four spec files. spec/controllers/projects/notes_controller_spec.rb asserts the GET issues no UPDATE or INSERT INTO "resource_label_events" and still renders the reference. spec/models/resource_label_event_spec.rb asserts the derive leaves the row untouched in the database. spec/services/issuable/discussions_list_service_spec.rb and spec/services/resource_events/merge_into_notes_service_spec.rb cover the other two production entry points into the render path, Issuable::DiscussionsListService and ResourceEvents::MergeIntoNotesService, since the fix is one shared model change and both reach it. All six fail before this change and pass after. The full notes_controller_spec file runs at 97 examples / 36 failures with the change and 95 / 36 without; the 36 are a local worktree asset gap (cannot load such file -- sass from app/helpers/vite_helper.rb) identical on both baselines, so there are no new failures.
Related to #628135