File: query_blocker.rb

package info (click to toggle)
ruby-sequel 5.97.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 11,188 kB
  • sloc: ruby: 123,115; makefile: 3
file content (172 lines) | stat: -rw-r--r-- 5,397 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# 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                   # 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){}
#
# Database#block_queries is useful for blocking queries inside
# the block.  However, there may be cases where you want to
# allow queries in specific places inside a block_queries block.
# You can use Database#allow_queries for that:
#
#   DB.block_queries do
#     DB.allow_queries do
#       DB[:table].all           # Query allowed
#     end
#
#     DB[:table].all           # Exception raised
#   end
#
# When mixing block_queries and allow_queries with scopes, the
# narrowest scope has priority.  So if you are blocking with
# :thread scope, and allowing with :fiber scope, queries in the
# current fiber will be allowed, but queries in different fibers of
# the current thread will be blocked.
#
# 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

require "fiber"

#
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

    # If checking a connection for validity, and a BlockedQuery exception is
    # raised, treat it as a valid connection.  You cannot check whether the
    # connection is valid without issuing a query, and if queries are blocked,
    # you need to assume it is valid or assume it is not.  Since it most cases
    # it will be valid, this assumes validity.
    def valid_connection?(conn)
      super
    rescue BlockedQuery
      true
    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
      b.fetch(Fiber.current) do
        b.fetch(Thread.current) do
          b.fetch(:global, false)
        end
      end
    end

    # Allow queries inside the block.  Only useful if they are already blocked
    # for the same scope. Useful for blocking queries generally, and only allowing
    # them in specific places.  Takes the same :scope option as #block_queries.
    def allow_queries(opts=OPTS, &block)
      _allow_or_block_queries(false, opts, &block)
    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, &block)
      _allow_or_block_queries(true, opts, &block)
    end
    
    private

    # Internals of block_queries and allow_queries.
    def _allow_or_block_queries(value, opts)
      scope = query_blocker_scope(opts)
      prev_value = nil
      scopes = @blocked_query_scopes

      begin
        Sequel.synchronize do
          prev_value = scopes[scope]
          scopes[scope] = value
        end

        yield
      ensure
        Sequel.synchronize do
          if prev_value.nil?
            scopes.delete(scope)
          else
            scopes[scope] = prev_value
          end
        end
      end
    end

    # The scope for the query block, either :global, or a Thread or Fiber instance.
    def query_blocker_scope(opts)
      case scope = opts[:scope]
      when nil
        :global
      when :global, Thread, Fiber
        scope
      when :thread
        Thread.current
      when :fiber
        Fiber.current
      else
        raise Sequel::Error, "invalid scope given to block_queries: #{scope.inspect}"
      end
    end

    # 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