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 =
Runtime::Cancel
SUPERVISOR_JOIN_TIMEOUT =
5.0
MAX_PAUSE_FAILURES =
3

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.



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

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
  @rr_slot = [0]
  @reconnects = 0
  @health_failures = 0
  @supervisor_error = nil
  @supervisor = nil
  @supervisor_wake = Runtime::Notification.new
  @pause_failures = 0

  @prepared_statements = {}
  @prepared_generation = 0
  @statement_sequence = 0
  @type_maps = TypeMaps.new

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

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

Instance Attribute Details

#pipeline_sizeObject (readonly)

Returns the value of attribute pipeline_size.



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

def pipeline_size
  @pipeline_size
end

#reconnectsObject (readonly)

Returns the value of attribute reconnects.



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

def reconnects
  @reconnects
end

Instance Method Details

#abort!Object



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/pg_pipeline/pool.rb', line 272

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

#await_prepares!(requests) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/pg_pipeline/pool.rb', line 181

def await_prepares!(requests)
  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
end

#bind_type_map(request, statement) ⇒ Object



118
119
120
121
122
123
124
125
# File 'lib/pg_pipeline/pool.rb', line 118

def bind_type_map(request, statement)
  return unless statement.typed?

  maps = @type_maps
  return unless maps

  maps.bind(request, statement)
end

#closing?Boolean

Returns:

  • (Boolean)


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

def closing? = @closing

#dispatch_prepare(statement) ⇒ Object



172
173
174
175
176
177
178
179
# File 'lib/pg_pipeline/pool.rb', line 172

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

  drivers.map { |driver| Request.prepare(statement).tap { |r| driver.submit(r) } }
end

#drop_prepared_statement(logical_name, statement) ⇒ Object



197
198
199
200
201
202
# File 'lib/pg_pipeline/pool.rb', line 197

def drop_prepared_statement(logical_name, statement)
  return unless @prepared_statements.delete(logical_name)

  @type_maps.unregister(statement.physical_name) if statement&.typed? && @type_maps
  @prepared_generation += 1
end

#graceful_closeObject



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/pg_pipeline/pool.rb', line 242

def graceful_close
  return unless @started

  if @pinned_owners[Fiber.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:



108
109
110
111
112
113
114
115
116
# File 'lib/pg_pipeline/pool.rb', line 108

def pipeline_driver
  ensure_available!

  driver = PoolOps.select_driver_into(@drivers, @rr, @rr_slot)
  @rr = @rr_slot[0]
  raise NotDispatchedError, "no live pipeline connections; request was not dispatched" unless driver

  driver
end

#prepare_statement(client, name, sql, param_types, typed: false) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/pg_pipeline/pool.rb', line 127

def prepare_statement(client, name, sql, param_types, typed: false)
  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 = register_prepared_statement(client, logical_name, sql, param_types, typed)
  requests = []

  begin
    requests = dispatch_prepare(statement)
    await_prepares!(requests)
    statement
  rescue Exception
    drop_prepared_statement(logical_name, statement)
    raise
  ensure
    requests.each { |request| request.cancel! unless request.settled? }
  end
end

#register_prepared_statement(client, logical_name, sql, param_types, typed) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/pg_pipeline/pool.rb', line 149

def register_prepared_statement(client, logical_name, sql, param_types, typed)
  @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,
    typed: typed
  )

  @prepared_statements[logical_name] = statement
  register_typed_maps(statement) if statement.typed?
  @prepared_generation += 1
  statement
end

#register_typed_maps(statement) ⇒ Object



166
167
168
169
170
# File 'lib/pg_pipeline/pool.rb', line 166

def register_typed_maps(statement)
  @type_maps ||= TypeMaps.new
  @type_maps.register(statement)
  @type_maps.ensure_bundle!(@connection_args)
end

#startObject

Raises:



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

def start
  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
  raise Error, "pool start requires an active Fiber scheduler" unless Fiber.scheduler

  begin
    @pipeline_size.times { @drivers << start_pipeline_driver }
    @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 = Runtime::Semaphore.new(@pinned_size) if @pinned_size.positive?
    @started = true
    @supervisor = Runtime.spawn(name: :supervisor) { supervise } if @reconnect || @health_check
  rescue Exception
    cleanup_partial_start
    raise
  end

  self
end

#statsObject



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/pg_pipeline/pool.rb', line 218

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,
    supervisor_alive: supervisor_alive?,
    closing: @closing,
    closed: @closed,
    pinned_error: @pinned_error&.message
  }
end

#with_pinnedObject

Raises:

  • (@pinned_error)


204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/pg_pipeline/pool.rb', line 204

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

  owner = Fiber.current
  assert_not_nested_pinned!(owner)

  @pinned_gate.acquire do
    assert_pinned_open!
    run_pinned(owner) { |conn| yield conn }
  end
end