Class: PgPipeline::Pool

Inherits:
Object
  • Object
show all
Defined in:
lib/pg_pipeline/pool.rb

Constant Summary collapse

DEFAULT_PIPELINE_SIZE =
4
DEFAULT_PINNED_SIZE =
2
DEFAULT_RECONNECT_INTERVAL =
0.5
DEFAULT_RECONNECT_BACKOFF_MAX =
30.0
DEFAULT_HEALTH_INTERVAL =
10.0
DEFAULT_HEALTH_TIMEOUT =
5.0
DISCARD_SEQUENCES_SERVER_VERSION =
90_400
CANCEL_SIGNAL =
Async::Cancel

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_args, pipeline_size: DEFAULT_PIPELINE_SIZE, pinned_size: DEFAULT_PINNED_SIZE, max_pending: ConnectionDriver::DEFAULT_MAX_PENDING, max_in_flight: ConnectionDriver::DEFAULT_MAX_IN_FLIGHT, reconnect: true, reconnect_interval: DEFAULT_RECONNECT_INTERVAL, reconnect_backoff_max: DEFAULT_RECONNECT_BACKOFF_MAX, health_check: true, health_interval: DEFAULT_HEALTH_INTERVAL, health_timeout: DEFAULT_HEALTH_TIMEOUT, cancel_pinned_on_abort: true) ⇒ Pool

Returns a new instance of Pool.



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
# File 'lib/pg_pipeline/pool.rb', line 26

def initialize(connection_args,
               pipeline_size: DEFAULT_PIPELINE_SIZE,
               pinned_size: DEFAULT_PINNED_SIZE,
               max_pending: ConnectionDriver::DEFAULT_MAX_PENDING,
               max_in_flight: ConnectionDriver::DEFAULT_MAX_IN_FLIGHT,
               reconnect: true,
               reconnect_interval: DEFAULT_RECONNECT_INTERVAL,
               reconnect_backoff_max: DEFAULT_RECONNECT_BACKOFF_MAX,
               health_check: true,
               health_interval: DEFAULT_HEALTH_INTERVAL,
               health_timeout: DEFAULT_HEALTH_TIMEOUT,
               cancel_pinned_on_abort: true)
  @connection_args = connection_args
  @pipeline_size = PoolOps.positive_integer!(pipeline_size, :pipeline_size)
  @pinned_size = PoolOps.nonnegative_integer!(pinned_size, :pinned_size)
  @max_pending = max_pending
  @max_in_flight = max_in_flight
  @reconnect = reconnect
  @reconnect_interval = PoolOps.finite_float!(reconnect_interval, :reconnect_interval)
  @reconnect_backoff_max = PoolOps.finite_float!(reconnect_backoff_max, :reconnect_backoff_max)
  @health_check = health_check
  @health_interval = PoolOps.finite_float!(health_interval, :health_interval, allow_zero: true)
  @health_timeout = PoolOps.finite_float!(health_timeout, :health_timeout)
  @cancel_pinned_on_abort = cancel_pinned_on_abort

  @drivers = []
  @driver_backoff = []
  @driver_attempts = []
  @driver_last_health = []
  @rr = 0
  @reconnects = 0
  @health_failures = 0
  @supervisor_error = nil
  @supervisor = nil
  @task_parent = nil

  @prepared_statements = {}
  @prepared_generation = 0
  @statement_sequence = 0

  @pinned_free = []
  @pinned_in_use = {}
  @pinned_gate = nil
  @pinned_error = nil
  @pinned_active = 0
  @pinned_owners = Hash.new(0)
  @pinned_idle = Async::Notification.new

  @started = false
  @closing = false
  @closed = false
end

Instance Attribute Details

#pipeline_sizeObject (readonly)

Returns the value of attribute pipeline_size.



24
25
26
# File 'lib/pg_pipeline/pool.rb', line 24

def pipeline_size
  @pipeline_size
end

#reconnectsObject (readonly)

Returns the value of attribute reconnects.



24
25
26
# File 'lib/pg_pipeline/pool.rb', line 24

def reconnects
  @reconnects
end

Instance Method Details

#abort!Object



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/pg_pipeline/pool.rb', line 257

def abort!
  return unless @started || @closing

  @closing = true
  @started = false
  first_error = nil

  begin
    first_error = preferred_shutdown_error(first_error, stop_supervisor)
    if @cancel_pinned_on_abort
      first_error = preferred_shutdown_error(first_error, cancel_in_use_pinned)
    end
    first_error = preferred_shutdown_error(first_error, close_all_drivers(:abort!))
  ensure
    close_free_pinned
    @closed = true
  end

  raise first_error if first_error

  nil
end

#closing?Boolean

Returns:

  • (Boolean)


104
# File 'lib/pg_pipeline/pool.rb', line 104

def closing? = @closing

#graceful_closeObject



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/pg_pipeline/pool.rb', line 227

def graceful_close
  return unless @started

  if @pinned_owners[Async::Task.current].positive?
    raise Error, "cannot close the pool from inside Client#session/transaction"
  end

  @closing = true
  @started = false
  first_error = nil

  begin
    first_error = preferred_shutdown_error(first_error, stop_supervisor)
    first_error = preferred_shutdown_error(first_error, close_all_drivers(:graceful_close))

    begin
      wait_for_pinned_idle
    rescue CANCEL_SIGNAL, StandardError => e
      first_error = preferred_shutdown_error(first_error, e)
    end
  ensure
    close_free_pinned
    @closed = true
  end

  raise first_error if first_error

  nil
end

#pipeline_driverObject

Raises:



106
107
108
109
110
111
112
113
# File 'lib/pg_pipeline/pool.rb', line 106

def pipeline_driver
  ensure_available!

  driver, @rr = PoolOps.select_driver(@drivers, @rr)
  raise NotDispatchedError, "no live pipeline connections; request was not dispatched" unless driver

  driver
end

#prepare_statement(client, name, sql, param_types) ⇒ Object



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
# File 'lib/pg_pipeline/pool.rb', line 115

def prepare_statement(client, name, sql, param_types)
  ensure_available!
  logical_name = PreparedStatementOps.snapshot_name(name)
  if @prepared_statements.key?(logical_name)
    raise Error, "prepared statement #{logical_name.inspect} already exists"
  end

  @statement_sequence += 1
  statement = PreparedStatement.new(
    client: client,
    name: logical_name,
    physical_name: "pgp_#{@statement_sequence.to_s(36)}",
    sql: sql,
    param_types: param_types
  )

  @prepared_statements[logical_name] = statement
  @prepared_generation += 1
  requests = []

  begin
    drivers = @drivers.select(&:available?)
    if drivers.empty?
      raise NotDispatchedError, "no live pipeline connections; statement was not prepared"
    end

    requests = drivers.map do |driver|
      Request.prepare(statement).tap { |request| driver.submit(request) }
    end

    first_error = nil
    requests.each do |request|
      begin
        RequestOps.clear_result(request.wait)
      rescue StandardError => e
        if first_error
          e.clear_result! if e.respond_to?(:clear_result!)
        else
          first_error = e
        end
      end
    end
    raise first_error if first_error

    statement
  rescue Exception
    if @prepared_statements.delete(logical_name)
      @prepared_generation += 1
    end
    raise
  ensure
    requests.each { |request| request.cancel! unless request.settled? }
  end
end

#start(parent: nil) ⇒ Object

Raises:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/pg_pipeline/pool.rb', line 79

def start(parent: nil)
  raise Error, "pool already started" if @started
  if @closing || @closed
    raise ShutdownError, "pool is closing or was closed and cannot be restarted; create a new Pool"
  end

  parent ||= Async::Task.current
  @task_parent = parent

  begin
    @pipeline_size.times { @drivers << start_pipeline_driver(parent) }
    @driver_backoff = Array.new(@drivers.size, 0.0)
    @driver_attempts = Array.new(@drivers.size, 0)
    @driver_last_health = Array.new(@drivers.size, monotonic)
    @pinned_gate = Async::Semaphore.new(@pinned_size) if @pinned_size.positive?
    @started = true
    @supervisor = parent.async { supervise } if @reconnect || @health_check
  rescue Exception
    cleanup_partial_start
    raise
  end

  self
end

#statsObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/pg_pipeline/pool.rb', line 204

def stats
  {
    pipeline: {
      size: @pipeline_size,
      live: @drivers.count(&:available?),
      drivers: @drivers.map(&:stats)
    },
    pinned: {
      size: @pinned_size,
      active: @pinned_active,
      free: @pinned_free.size,
      in_use: @pinned_in_use.size
    },
    prepared_statements: (@prepared_statements || {}).size,
    reconnects: @reconnects,
    health_failures: @health_failures,
    supervisor_error: @supervisor_error&.message,
    closing: @closing,
    closed: @closed,
    pinned_error: @pinned_error&.message
  }
end

#with_pinnedObject

Raises:

  • (@pinned_error)


170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/pg_pipeline/pool.rb', line 170

def with_pinned
  ensure_available!
  raise @pinned_error if @pinned_error
  raise Error, "pinned pool is disabled (pinned_size=0)" if @pinned_size.zero?

  if @pinned_owners[Async::Task.current].positive?
    raise RecursiveCheckoutError,
          "nested Client#session/transaction on the same task is not allowed; " \
          "use Transaction#savepoint for nested atomicity"
  end

  @pinned_gate.acquire do
    raise @pinned_error if @pinned_error
    raise ShutdownError, "pool is closing" if @closing

    owner = Async::Task.current
    conn = nil
    @pinned_active += 1
    @pinned_owners[owner] += 1

    begin
      conn = @pinned_free.pop || PoolOps.new_connection(@connection_args)
      raise @pinned_error if @pinned_error
      raise ShutdownError, "pool is closing" if @closing

      @pinned_in_use[conn] = owner
      yield conn
    ensure
      @pinned_in_use.delete(conn) if conn
      release_pinned(owner, conn)
    end
  end
end