Bound native fragment caching and collapse concurrent page renders

This commit is contained in:
GPT on behalf of DHH
2026-10-07 21:38:36 +02:00
parent b220486c16
commit 8d02540e31
7 changed files with 254 additions and 30 deletions
+125
View File
@@ -0,0 +1,125 @@
require "test_helper"
class FragmentRenderingTest < ActionDispatch::IntegrationTest
self.use_transactional_tests = false
setup do
host! "once.campfire.test"
sign_in :david
@previous_caching = ActionController::Base.perform_caching
@previous_message_caching = MessagesController.perform_caching
@class_store = ApplicationController.cache_store
@global_store = Rails.cache
ActionController::Base.perform_caching = true
MessagesController.perform_caching = true
FragmentCache.store.clear
ResponseCache.instance.clear
@room = rooms(:watercooler)
@message = @room.messages.ordered.last
end
teardown do
ActionController::Base.perform_caching = @previous_caching
MessagesController.perform_caching = @previous_message_caching
FragmentCache.store.clear
ResponseCache.instance.clear
end
test "native collection caching uses the bounded store without replacing shared stores" do
assert_same FragmentCache.store, ApplicationController.new.cache_store
assert_same FragmentCache.store, ActionView::PartialRenderer.collection_cache
assert_same @class_store, ApplicationController.cache_store
assert_same @global_store, Rails.cache
ResponseCache.instance.stubs(:budget).returns(0)
get room_messages_url(@room)
assert_response :success
hits = []
ActiveSupport::Notifications.subscribed(->(event) { hits.concat(event.payload[:hits]) }, "cache_read_multi.active_support") do
get room_messages_url(@room)
end
assert_response :success
assert_not_empty hits
end
test "disabled page caching retains fresh native fragments after foreign leaf writes" do
ResponseCache.instance.stubs(:budget).returns(0)
get room_messages_url(@room)
assert_response :success
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "foreign disabled body", @message.id)
get room_messages_url(@room)
assert_response :success
assert_includes response.body, "foreign disabled body"
end
test "conditional pagination invalidates warmed native fragments without timestamp changes" do
get room_messages_url(@room), headers: { "If-None-Match" => "unmatched" }
assert_response :success
etag = response.headers["ETag"]
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "foreign conditional body", @message.id)
get room_messages_url(@room), headers: { "If-None-Match" => etag }
assert_response :success
assert_includes response.body, "foreign conditional body"
end
test "refresh streams recheck native cached creator body and boost presentation" do
message = messages(:first)
room = message.room
get room_refresh_url(room, format: :turbo_stream), params: { since: 0 }
assert_response :success
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "foreign refreshed body", message.id)
foreign_write("UPDATE users SET name = ? WHERE id = ?", "Foreign refreshed creator", message.creator_id)
foreign_write("UPDATE boosts SET content = ? WHERE message_id = ?", "Foreign refreshed boost", message.id)
get room_refresh_url(room, format: :turbo_stream), params: { since: 0 }
assert_response :success
assert_includes response.body, "foreign refreshed body"
assert_includes response.body, "Foreign refreshed creator"
assert_includes response.body, "Foreign refreshed boost"
end
test "Jbuilder fragments use the bounded store and do not create HTML token state" do
api = open_session { |session| session.host! "once.campfire.test" }
path = room_bot_messages_path(@room, users(:bender).bot_key)
api.get path
assert_equal 200, api.response.status
assert_not api.cookies["_campfire_session"]
hits = []
ActiveSupport::Notifications.subscribed(->(event) { hits << event.payload[:key] if event.payload[:hit] }, "cache_read.active_support") do
api.get path
end
assert_equal 200, api.response.status
assert_not_empty hits
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "foreign JSON body", @message.id)
foreign_write("UPDATE users SET name = ? WHERE id = ?", "Foreign JSON creator", @message.creator_id)
api.get path
json = JSON.parse(api.response.body).find { |message| message["id"] == @message.id }
assert_includes json.dig("body", "html"), "foreign JSON body"
assert_equal "Foreign JSON creator", json.dig("creator", "name")
api.host! "another.campfire.test:8081"
api.get path
json = JSON.parse(api.response.body).find { |message| message["id"] == @message.id }
assert_includes json["url"], "another.campfire.test:8081"
assert_same @global_store, Rails.cache
end
test "detached broadcasts bypass fragments without a pre-render model snapshot" do
get room_messages_url(@room)
foreign_write("UPDATE action_text_rich_texts SET body = ? WHERE record_type = 'Message' AND record_id = ?", "foreign broadcast body", @message.id)
FragmentCache.store.expects(:read).never
FragmentCache.store.expects(:write).never
@message.reload.broadcast_create
assert_rendered_turbo_stream_broadcast @room, :messages, action: "append", target: [ @room, :messages ] do |stream|
assert_includes stream.to_html, "foreign broadcast body"
end
body = ApplicationController.render(partial: "messages/message", locals: { message: @message.reload })
assert_includes body, "foreign broadcast body"
assert_not ApplicationController.new.perform_caching
end
private
def foreign_write(sql, *bindings)
SQLite3::Database.new(ActiveRecord::Base.connection_db_config.database) do |database|
database.execute(sql, bindings)
end
ActiveRecord::Base.clear_query_caches_for_current_thread
end
end
+32 -1
View File
@@ -44,12 +44,43 @@ class ResponseCacheTest < ActiveSupport::TestCase
assert_nil @cache.read("k" * 2049, version)
end
test "observer namespaces cannot reuse old shared fragment keys after restart" do
test "clearing the observer gives later pages a distinct namespace" do
version = @cache.version
@cache.clear
assert_not_equal version, @cache.version
end
test "cold renders coalesce without holding the observer mutex" do
version = @cache.version
entered = Queue.new
release = Queue.new
first = Thread.new do
@cache.synchronize_render("page", version) do
entered << true
release.pop
@cache.write("page", version, entry("completed render"))
end
end
entered.pop
second_started = Queue.new
second = Thread.new do
second_started << true
@cache.synchronize_render("page", version) { @cache.read("page", version) }
end
second_started.pop
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
Thread.pass until second.status == "sleep" || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
assert_equal "sleep", second.status
assert_nil @cache.read("page", version)
assert_equal version, @cache.version
release << true
first.value
assert_equal "completed render", second.value[:body]
ensure
release << true if release
[ first, second ].compact.each { |thread| thread.join(2) }
end
private
def entry(body)
{ body: body, marker: "unexposed-marker", headers: {} }