diff --git a/app/controllers/concerns/authentication/session_lookup.rb b/app/controllers/concerns/authentication/session_lookup.rb index b180f21..f3dafb5 100644 --- a/app/controllers/concerns/authentication/session_lookup.rb +++ b/app/controllers/concerns/authentication/session_lookup.rb @@ -1,7 +1,12 @@ module Authentication::SessionLookup def find_session_by_cookie if token = cookies.signed[:session_token] - Session.find_by(token: token) + version = @response_cache_version if @response_cache_version && (request.get? || request.head?) + session, user = RecordCache.fetch([ "session", Digest::SHA256.hexdigest(token.to_s) ], version) do + session = Session.find_by(token: token) + [ session, session&.user ] + end + session&.tap { |record| record.user = user } end end end diff --git a/app/controllers/concerns/cached_responses.rb b/app/controllers/concerns/cached_responses.rb index e2458b7..95fa2a0 100644 --- a/app/controllers/concerns/cached_responses.rb +++ b/app/controllers/concerns/cached_responses.rb @@ -35,6 +35,10 @@ module CachedResponses end private + def read_record_cache_version + @response_cache_version if request.get? || request.head? + end + def capture_response_cache_version # Capture for native HTML/JSON/stream renders too, even with page reuse off. # Detached renderers do not run callbacks and therefore render uncached. @@ -81,6 +85,10 @@ module CachedResponses def cache_completed_response(key, original_session, encoding) if response.status == 200 && response.media_type == "text/html" && session.to_hash == original_session && !response.headers["Content-Encoding"] body = encoding == "gzip" ? Zlib.gzip(response.body) : response.body + unless body.empty? || response.headers["ETag"] || response.headers["Last-Modified"] + # Match Rack::ETag once, rather than hashing the same bytes on each hit. + response.headers["ETag"] = %(W/"#{Digest::SHA256.hexdigest(body).byteslice(0, 32)}") + end response.headers["Content-Encoding"] = "gzip" if encoding == "gzip" response.headers["Vary"] = (response.headers["Vary"].to_s.split(/,\s*/) | [ "Accept-Encoding" ]).join(", ") headers = response.headers.slice(*CACHE_HEADERS).to_h.freeze diff --git a/app/controllers/concerns/room_scoped.rb b/app/controllers/concerns/room_scoped.rb index 2af0991..deae415 100644 --- a/app/controllers/concerns/room_scoped.rb +++ b/app/controllers/concerns/room_scoped.rb @@ -7,7 +7,10 @@ module RoomScoped private def set_room - @membership = Current.user.memberships.find_by!(room_id: params[:room_id]) - @room = @membership.room + @membership, @room = RecordCache.fetch([ "membership", Current.user.id, params[:room_id] ], read_record_cache_version) do + membership = Current.user.memberships.find_by!(room_id: params[:room_id]) + [ membership, membership.room ] + end + @membership.room = @room end end diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb index 7641260..7e16cb4 100644 --- a/app/controllers/rooms_controller.rb +++ b/app/controllers/rooms_controller.rb @@ -21,7 +21,10 @@ class RoomsController < ApplicationController private def set_room - if room = room_scope.find_by(id: params[:room_id] || params[:id]) + room = RecordCache.fetch([ "room", self.class.name, Current.user.id, params[:room_id] || params[:id] ], read_record_cache_version) do + [ room_scope.find_by(id: params[:room_id] || params[:id]) ] + end.first + if room @room = room else redirect_to root_url, alert: "Room not found or inaccessible" diff --git a/app/models/record_cache.rb b/app/models/record_cache.rb new file mode 100644 index 0000000..03924a6 --- /dev/null +++ b/app/models/record_cache.rb @@ -0,0 +1,24 @@ +# Rebuild request-local models from bounded, immutable database snapshots. +# The observer must still see the captured epoch after lookup and admission. +class RecordCache + def self.fetch(key, version) + return yield unless version && ResponseCache.instance.budget.positive? && !ActiveRecord::Base.connection.transaction_open? + + key = ActiveSupport::Cache.expand_cache_key([ "record-snapshot-v1", version, key ]) + return yield if key.bytesize > ResponseCache::MAX_KEY_BYTES + + if snapshot = FragmentCache.store.read(key) + if ResponseCache.instance.version == version + return ActiveSupport::JSON.decode(snapshot).map { |name, attributes| name.constantize.instantiate(attributes) } + end + end + + records = yield + if records.all? { |record| record&.persisted? && !record.changed? } && ResponseCache.instance.version == version + # Raw database values retain timestamp precision and native enum binding. + snapshot = ActiveSupport::JSON.encode(records.map { |record| [ record.class.name, record.attributes_before_type_cast ] }) + FragmentCache.store.write(key, snapshot) if snapshot.bytesize <= ResponseCache::MAX_ENTRY_BYTES + end + records + end +end diff --git a/app/models/room/message_pusher.rb b/app/models/room/message_pusher.rb index e5e3c64..776e596 100644 --- a/app/models/room/message_pusher.rb +++ b/app/models/room/message_pusher.rb @@ -6,9 +6,9 @@ class Room::MessagePusher end def push - build_payload.tap do |payload| - push_to_users_involved_in_everything(payload) - push_to_users_involved_in_mentions(payload) + subscriptions = push_subscriptions_for_users_involved_in_everything.or(push_subscriptions_for_mentionable_users(message.mentionees)) + if subscriptions.exists? + Rails.configuration.x.web_push_pool.queue(build_payload, subscriptions) end end @@ -37,14 +37,6 @@ class Room::MessagePusher } end - def push_to_users_involved_in_everything(payload) - enqueue_payload_for_delivery payload, push_subscriptions_for_users_involved_in_everything - end - - def push_to_users_involved_in_mentions(payload) - enqueue_payload_for_delivery payload, push_subscriptions_for_mentionable_users(message.mentionees) - end - def push_subscriptions_for_users_involved_in_everything relevant_subscriptions.merge(Membership.involved_in_everything) end @@ -59,10 +51,6 @@ class Room::MessagePusher Push::Subscription .joins(user: :memberships) .merge(User.active) - .merge(Membership.visible.disconnected.where(room: room).where.not(user: message.creator)) - end - - def enqueue_payload_for_delivery(payload, subscriptions) - Rails.configuration.x.web_push_pool.queue(payload, subscriptions) + .merge(Membership.visible.disconnected.where(room: room).where.not(user_id: message.creator_id)) end end diff --git a/app/views/messages/_actions.html.erb b/app/views/messages/_actions.html.erb index ab6f2ec..48973d6 100644 --- a/app/views/messages/_actions.html.erb +++ b/app/views/messages/_actions.html.erb @@ -11,8 +11,9 @@