118 lines
5.7 KiB
Ruby
118 lines
5.7 KiB
Ruby
# Proves, over a real TCP socket (not just a direct Application.call like
|
|
# test/test_application_n_plus_one.rb), what examples/blog/app.rb's
|
|
# PRISM_BLOG_STRICT_N_PLUS_ONE=1 gate actually buys: before this session,
|
|
# Orm::NPlusOneDetector.report! was called AFTER Application#dispatch's
|
|
# instrumented span had already closed its rescue reach, so a violation blew
|
|
# straight out of #dispatch, past every middleware, and past
|
|
# Server#serve's `@app.call(env)` too (server.rb only rescues
|
|
# Errno::EPIPE/Errno::ECONNRESET/IOError around that call, never plain
|
|
# StandardError) - the request thread died with the socket just closed and
|
|
# NOT ONE byte written back, i.e. a client seeing a broken connection, not an
|
|
# HTTP response. #dispatch now rescues the violation in the same place as
|
|
# any other bug and routes it through handle_uncaught_exception, so
|
|
# raise_on_violation actually behaves like every other exception in this
|
|
# framework: a normal 500 a curl/CI run can see and assert on.
|
|
#
|
|
# Uses a tiny synthetic app (not examples/blog, which is deliberately kept
|
|
# N+1-free) with ONE route that has a real, deliberately-unfixed N+1 -
|
|
# reintroducing a genuine bug is exactly what a regression gate needs to be
|
|
# proven against; asserting against blog's own already-clean routes would
|
|
# only prove "doesn't false-positive," not "catches a real regression."
|
|
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
|
require 'prism'
|
|
require 'net/http'
|
|
require 'tmpdir'
|
|
require 'stringio'
|
|
|
|
dir = Dir.mktmpdir('prism-strict-n-plus-one-demo')
|
|
store = Prism::Orm::Store.new(File.join(dir, 'demo.pstore'))
|
|
store.create_table(:authors, %w[id name])
|
|
store.create_table(:posts, %w[id title author_id])
|
|
|
|
class DemoAuthor < Prism::Orm::Model
|
|
table_name :authors
|
|
has_many :posts, class_name: 'DemoPost', foreign_key: 'author_id'
|
|
end
|
|
|
|
class DemoPost < Prism::Orm::Model
|
|
table_name :posts
|
|
belongs_to :author, class_name: 'DemoAuthor', foreign_key: 'author_id'
|
|
end
|
|
|
|
[DemoAuthor, DemoPost].each { |klass| klass.store = store }
|
|
|
|
3.times do |i|
|
|
author = DemoAuthor.create!(name: "author#{i}")
|
|
DemoPost.create!(title: "post#{i}", author_id: author.id)
|
|
end
|
|
|
|
log = StringIO.new
|
|
|
|
class DemoApp < Prism::Application
|
|
# Genuinely N+1: no .includes(:posts), so each author's #posts is a lazy
|
|
# cache-miss load - exactly the pattern examples/blog's PostsController
|
|
# used to have on Comment#likes before it was found and fixed (see
|
|
# claudocs/plan/prism/stan.md, N+1 detector session).
|
|
get('/regression') do
|
|
names = DemoAuthor.all.to_a.map { |author| "#{author.name}: #{author.posts.count} post(s)" }
|
|
names.join(', ')
|
|
end
|
|
|
|
# Same query, fixed the way PostsController actually was: eager-loaded, so
|
|
# no lazy load ever fires and the detector has nothing to count.
|
|
get('/clean') do
|
|
names = DemoAuthor.all.includes(:posts).to_a.map { |author| "#{author.name}: #{author.posts.size} post(s)" }
|
|
names.join(', ')
|
|
end
|
|
end
|
|
|
|
def get(port, path)
|
|
Net::HTTP.start('127.0.0.1', port) { |http| http.get(path) }
|
|
end
|
|
|
|
puts '=' * 60
|
|
puts 'phase 1: raise_on_violation: true (PRISM_BLOG_STRICT_N_PLUS_ONE=1 equivalent)'
|
|
Prism::Orm::NPlusOneDetector.enable!(threshold: 2, io: log, raise_on_violation: true)
|
|
server = Prism::Server.new(DemoApp, port: 0).start
|
|
port = server.port
|
|
|
|
response = get(port, '/regression')
|
|
puts "GET /regression -> #{response.code} #{response['Content-Type']} body=#{response.body.inspect}"
|
|
raise "expected a real 500, got #{response.code}" unless response.code == '500'
|
|
raise 'expected the normal uncaught-exception body, not something ad hoc' unless response.body == 'Internal Server Error'
|
|
raise 'expected the violation to still have been logged to the detector io' unless log.string.include?('N+1 query detected: DemoAuthor#posts')
|
|
puts 'confirmed: a real 500 response, not a broken connection - the violation was logged, exactly like any other bug'
|
|
|
|
# The connection dying silently (the pre-fix behavior) would have made this
|
|
# second request on a FRESH connection unremarkable either way - the real
|
|
# proof is that the server thread handling /regression is still alive and
|
|
# the process itself never crashed, which this second, unrelated request
|
|
# after the violation demonstrates.
|
|
follow_up = get(port, '/clean')
|
|
puts "GET /clean (after the violation) -> #{follow_up.code} body=#{follow_up.body.inspect}"
|
|
raise "expected the clean route to still work fine after a violation elsewhere, got #{follow_up.code}" unless follow_up.code == '200'
|
|
raise 'expected includes(:posts) to prevent any lazy load / violation' if log.string.scan('N+1 query detected').size > 1
|
|
puts 'confirmed: eager-loaded route stays silent and the server keeps serving requests after a violation'
|
|
|
|
Prism::Orm::NPlusOneDetector.disable!
|
|
Prism::Orm::NPlusOneDetector.reset!
|
|
|
|
puts '-' * 60
|
|
puts 'phase 2: log-only mode (raise_on_violation: false, the default when PRISM_BLOG_STRICT_N_PLUS_ONE is unset)'
|
|
log2 = StringIO.new
|
|
Prism::Orm::NPlusOneDetector.enable!(threshold: 2, io: log2, raise_on_violation: false)
|
|
|
|
response2 = get(port, '/regression')
|
|
puts "GET /regression -> #{response2.code} body=#{response2.body.inspect}"
|
|
raise "expected a normal 200 in log-only mode, got #{response2.code}" unless response2.code == '200'
|
|
raise 'expected the SAME regression to still be logged, just not raised' unless log2.string.include?('N+1 query detected: DemoAuthor#posts')
|
|
puts 'confirmed: same regression, same log line, but the response is a plain 200 - nothing breaks for real traffic ' \
|
|
'when the strict flag is off'
|
|
|
|
Prism::Orm::NPlusOneDetector.disable!
|
|
Prism::Orm::NPlusOneDetector.reset!
|
|
|
|
puts '=' * 60
|
|
puts 'strict_n_plus_one_demo done: raise_on_violation now surfaces as a normal 500 over real HTTP (never a dead ' \
|
|
'connection), and the same underlying regression is silent-but-logged when the strict flag is off.'
|