-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add query_blocker extension, for blocking queries inside a block
This is useful for ensuring a given block of code does not execute any queries. For use in concurrent programs, the query_blocker takes a :scope option for the scope of the block.
- Loading branch information
1 parent
1fe32a5
commit f9b3a74
Showing
6 changed files
with
291 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# frozen-string-literal: true | ||
# | ||
# The query_blocker extension adds Database#block_queries. | ||
# Inside the block passed to #block_queries, any attempts to | ||
# execute a query/statement on the database will raise a | ||
# Sequel::QueryBlocker::BlockedQuery exception. | ||
# | ||
# DB.extension :query_blocker | ||
# DB.block_queries do | ||
# ds = DB[:table] # No exception | ||
# ds = ds.where(column: 1) # No exception | ||
# ds.all # Attempts query, exception raised | ||
# end | ||
# | ||
# To handle concurrency, you can pass a :scope option: | ||
# | ||
# # Current Thread | ||
# DB.block_queries(scope: :thread){} | ||
# | ||
# # Current Fiber | ||
# DB.block_queries(scope: :fiber){} | ||
# | ||
# # Specific Thread | ||
# DB.block_queries(scope: Thread.current){} | ||
# | ||
# # Specific Fiber | ||
# DB.block_queries(scope: Fiber.current){} | ||
# | ||
# Note that this should catch all queries executed through the | ||
# Database instance. Whether it catches queries executed directly | ||
# on a connection object depends on the adapter in use. | ||
# | ||
# Related module: Sequel::QueryBlocker | ||
|
||
# :nocov: | ||
require "fiber" if RUBY_VERSION <= "2.7" | ||
# :nocov: | ||
|
||
# | ||
module Sequel | ||
module QueryBlocker | ||
# Exception class raised if there is an attempt to execute a | ||
# query/statement on the database inside a block passed to | ||
# block_queries. | ||
class BlockedQuery < Sequel::Error | ||
end | ||
|
||
def self.extended(db) | ||
db.instance_exec do | ||
@blocked_query_scopes ||= {} | ||
end | ||
end | ||
|
||
# Check whether queries are blocked before executing them. | ||
def log_connection_yield(sql, conn, args=nil) | ||
# All database adapters should be calling this method around | ||
# query execution (otherwise the queries would not get logged), | ||
# ensuring the blocking is checked. Any database adapter issuing | ||
# a query without calling this method is considered buggy. | ||
check_blocked_queries! | ||
super | ||
end | ||
|
||
# Whether queries are currently blocked. | ||
def block_queries? | ||
b = @blocked_query_scopes | ||
Sequel.synchronize{b[:global] || b[Thread.current] || b[Fiber.current]} || false | ||
end | ||
|
||
# Reject (raise an BlockedQuery exception) if there is an attempt to execute | ||
# a query/statement inside the block. | ||
# | ||
# The :scope option indicates which queries are rejected inside the block: | ||
# | ||
# :global :: This is the default, and rejects all queries. | ||
# :thread :: Reject all queries in the current thread. | ||
# :fiber :: Reject all queries in the current fiber. | ||
# Thread :: Reject all queries in the given thread. | ||
# Fiber :: Reject all queries in the given fiber. | ||
def block_queries(opts=OPTS) | ||
case scope = opts[:scope] | ||
when nil | ||
scope = :global | ||
when :global | ||
# nothing | ||
when :thread | ||
scope = Thread.current | ||
when :fiber | ||
scope = Fiber.current | ||
when Thread, Fiber | ||
# nothing | ||
else | ||
raise Sequel::Error, "invalid scope given to block_queries: #{scope.inspect}" | ||
end | ||
|
||
prev_value = nil | ||
scopes = @blocked_query_scopes | ||
|
||
begin | ||
Sequel.synchronize do | ||
prev_value = scopes[scope] | ||
scopes[scope] = true | ||
end | ||
|
||
yield | ||
ensure | ||
Sequel.synchronize do | ||
if prev_value | ||
scopes[scope] = prev_value | ||
else | ||
scopes.delete(scope) | ||
end | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
# Raise a BlockQuery exception if queries are currently blocked. | ||
def check_blocked_queries! | ||
raise BlockedQuery, "cannot execute query inside a block_queries block" if block_queries? | ||
end | ||
end | ||
|
||
Database.register_extension(:query_blocker, QueryBlocker) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
require_relative "spec_helper" | ||
|
||
describe "query_blocker extension" do | ||
fiber_is_thread = RUBY_ENGINE == 'jruby' && Fiber.new{Thread.current}.resume != Thread.current | ||
|
||
before do | ||
@db = Sequel.mock(:extensions=>[:query_blocker]) | ||
@ds = @db[:items] | ||
end | ||
|
||
it "#block_queries should block queries globally inside the block when called without options" do | ||
@ds.all.must_equal [] | ||
proc{@db.block_queries{@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries{Thread.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.join} | ||
@ds.all.must_equal [] | ||
end | ||
|
||
it "#block_queries should block queries globally inside the block when called with scope: :global" do | ||
@ds.all.must_equal [] | ||
proc{@db.block_queries(:scope=>:global){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:global){Thread.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.join} | ||
@ds.all.must_equal [] | ||
end | ||
|
||
it "#block_queries should block queries inside the current thread when called with scope: :thread" do | ||
@ds.all.must_equal [] | ||
proc{@db.block_queries(:scope=>:thread){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:thread){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:thread){Fiber.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.resume} unless fiber_is_thread | ||
@ds.all.must_equal [] | ||
end | ||
|
||
it "#block_queries should block queries inside the current fiber when called with scope: :fiber" do | ||
@ds.all.must_equal [] | ||
proc{@db.block_queries(:scope=>:fiber){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:fiber){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:fiber){Fiber.new{@ds.all}.resume}.must_equal [] | ||
@ds.all.must_equal [] | ||
end | ||
|
||
it "#block_queries should block queries inside the given thread when called with scope: Thread" do | ||
@ds.all.must_equal [] | ||
proc{@db.block_queries(:scope=>Thread.current){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>Thread.current){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>Thread.current){Fiber.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.resume} unless fiber_is_thread | ||
@ds.all.must_equal [] | ||
end | ||
|
||
it "#block_queries should block queries inside the given fiber when called with scope: Fiber" do | ||
@ds.all.must_equal [] | ||
proc{@db.block_queries(:scope=>Fiber.current){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>Fiber.current){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>Fiber.current){Fiber.new{@ds.all}.resume}.must_equal [] | ||
@ds.all.must_equal [] | ||
end | ||
|
||
it "#block_queries should raise Error if called with unsupported :scope option" do | ||
proc{@db.block_queries(:scope=>Object.new){}}.must_raise Sequel::Error | ||
end | ||
|
||
it "#block_queries should handle nested usage" do | ||
@ds.all.must_equal [] | ||
Thread.new{@ds.all}.value.must_equal [] | ||
Fiber.new{@ds.all}.resume.must_equal [] | ||
|
||
@db.block_queries(scope: :fiber) do | ||
proc{@db.block_queries(:scope=>:fiber){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:fiber){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:fiber){Fiber.new{@ds.all}.resume}.must_equal [] | ||
|
||
@db.block_queries(scope: :fiber) do | ||
proc{@db.block_queries(:scope=>:fiber){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:fiber){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:fiber){Fiber.new{@ds.all}.resume}.must_equal [] | ||
end | ||
|
||
@db.block_queries(scope: :thread) do | ||
proc{@db.block_queries(:scope=>:thread){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:thread){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:thread){Fiber.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.resume} unless fiber_is_thread | ||
|
||
@db.block_queries(scope: :thread) do | ||
proc{@db.block_queries(:scope=>:thread){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:thread){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:thread){Fiber.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.resume} unless fiber_is_thread | ||
end | ||
|
||
@db.block_queries do | ||
proc{@ds.all}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
proc{@db.block_queries{@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries{Thread.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.join} | ||
|
||
@db.block_queries do | ||
proc{@ds.all}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
proc{@db.block_queries{@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries{Thread.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.join} | ||
end | ||
|
||
proc{@ds.all}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
proc{@db.block_queries{@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries{Thread.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.join} | ||
end | ||
|
||
proc{@db.block_queries(:scope=>:thread){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:thread){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:thread){Fiber.new{assert_raises(Sequel::QueryBlocker::BlockedQuery){@ds.all}}.resume} unless fiber_is_thread | ||
end | ||
|
||
proc{@db.block_queries(:scope=>:fiber){@ds.all}}.must_raise Sequel::QueryBlocker::BlockedQuery | ||
@db.block_queries(:scope=>:fiber){Thread.new{@ds.all}.value}.must_equal [] | ||
@db.block_queries(:scope=>:fiber){Fiber.new{@ds.all}.resume}.must_equal [] | ||
end | ||
|
||
@ds.all.must_equal [] | ||
Thread.new{@ds.all}.value.must_equal [] | ||
Fiber.new{@ds.all}.resume.must_equal [] | ||
end | ||
|
||
it "#block_queries? should check whether queries are currently blocked" do | ||
@db.block_queries?.must_equal false | ||
@db.block_queries{@db.block_queries?}.must_equal true | ||
@db.block_queries?.must_equal false | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters