Retain content-validated message fragments across unrelated commits

This commit is contained in:
GPT on behalf of DHH
2026-10-08 12:00:45 +02:00
parent 59e1c6d3d7
commit 74381e8b58
10 changed files with 48 additions and 9 deletions
@@ -1,7 +1,7 @@
module Authentication::SessionLookup
def find_session_by_cookie
if token = cookies.signed[:session_token]
Session.eager_load(:user).find_by(token: token)
Session.find_by(token: token)
end
end
end
+4 -3
View File
@@ -26,11 +26,12 @@ module CachedResponses
end
def combined_fragment_cache_key(key)
@fragment_cache_namespace ||= [
@response_cache_version, request.base_url, request.script_name, request.format.to_s, I18n.locale,
@fragment_cache_context ||= [
request.base_url, request.script_name, request.format.to_s, I18n.locale,
Current.user&.id, (Digest::SHA256.hexdigest(Current.session.token) if Current.session)
].freeze
super([ @fragment_cache_namespace, key ])
version = @response_cache_version unless Array(key).flatten.any? { |part| part.is_a?(FragmentCache::ContentKey) }
super([ version, @fragment_cache_context, key ])
end
private
+16
View File
@@ -83,6 +83,22 @@ module MessagesHelper
FragmentCache.store.fetch(key) { capture(&block) }
end
def message_fragment_cache_key(message)
return message unless controller.perform_caching &&
%i[room creator rich_text_body boosts attachment_attachment].all? { |name| message.association(name).loaded? }
room = message.room
body = message.body.body
return message if room.direct? || message.attachment? || !body || body.to_html.include?("<action-text-attachment") ||
message.boosts.any? { |boost| !boost.association(:booster).loaded? }
# Collection caching preloads these rows before deriving keys. Read their
# actual content: foreign SQL writers need not touch Rails timestamps.
dependencies = [ message.attributes, room.attributes.slice("id", "type", "name"), message.creator.attributes,
body.to_html, message.boosts.sort_by(&:created_at).map { |boost| [ boost.attributes, boost.booster.attributes ] } ]
FragmentCache::ContentKey.new(Digest::SHA256.hexdigest(ActiveSupport::JSON.encode(dependencies)))
end
private
def text_message_presentation(body)
render = -> do
+7
View File
@@ -1,5 +1,12 @@
# Native view caches share one byte budget across every database generation.
class FragmentCache
# Only callers that fingerprint every rendered dependency may omit the epoch.
ContentKey = Data.define(:digest) do
def cache_key
digest
end
end
STORE = ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes)
private_constant :STORE
+1 -1
View File
@@ -1,7 +1,7 @@
<%# Be sure to check/update messages/_template.html.erb when changing this file %>
<%# Bump this version when the message presentation filters change what they emit. Editing this line changes the template digest, which busts BOTH this fragment cache and the collection cache that keys on this partial's digest (helper Ruby changes alone don't). %>
<% cache [ message, "presentation-v6" ] do %>
<% cache [ message_fragment_cache_key(message), "presentation-v7" ] do %>
<%= message_tag message do %>
<h2 class="message__day-separator"><%= local_datetime_tag message.created_at, style: :date %></h2>
+1 -1
View File
@@ -1 +1 @@
<%= render partial: "messages/message", collection: @messages, cached: true %>
<%= render partial: "messages/message", collection: @messages, cached: ->(message) { message_fragment_cache_key(message) } %>
@@ -1,5 +1,5 @@
<%= turbo_stream.append dom_id(@room, :messages) do %>
<%= render partial: "messages/message", collection: @new_messages, cached: true %>
<%= render partial: "messages/message", collection: @new_messages, cached: ->(message) { message_fragment_cache_key(message) } %>
<% end if @new_messages.any? %>
<% @updated_messages.each do |message| %>
+1 -1
View File
@@ -14,7 +14,7 @@
<%= messages_tag(@room) do %>
<%= render "rooms/show/invitation", room: @room %>
<%= render partial: "messages/message", collection: @messages, cached: true %>
<%= render partial: "messages/message", collection: @messages, cached: ->(message) { message_fragment_cache_key(message) } %>
<% end %>
<%= turbo_stream_from @room, :messages, channel: "RoomMessagesChannel" %>
+1 -1
View File
@@ -54,7 +54,7 @@
</div>
<%= search_results_tag do %>
<%= render partial: "messages/message", collection: @messages, cached: true %>
<%= render partial: "messages/message", collection: @messages, cached: ->(message) { message_fragment_cache_key(message) } %>
<% end %>
</div>
+15
View File
@@ -74,6 +74,21 @@ class FragmentRenderingTest < ActionDispatch::IntegrationTest
assert_select "form[action=?]", message_boosts_path(@message)
end
test "a commit in another room retains content-validated message collection hits" do
ResponseCache.instance.stubs(:budget).returns(0)
get room_messages_url(@room)
assert_response :success
foreign_write("UPDATE rooms SET name = ? WHERE id = ?", "Unrelated room rename", rooms(:pets).id)
collections = []
ActiveSupport::Notifications.subscribed(->(event) { collections << event.payload }, "render_collection.action_view") do
get room_messages_url(@room)
end
assert_response :success
messages = collections.find { |payload| payload[:identifier].end_with?("messages/_message.html.erb") }
assert messages
assert_equal @room.messages.count, messages[:cache_hits]
end
test "a foreign commit after capture bypasses old native fragment lookups" do
ResponseCache.instance.stubs(:budget).returns(0)
get room_messages_url(@room)