pg_pipeline 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7135257ec52d199f4a7e0d3666909eebd8effa277f7efe80d27ca8ff00717f93
4
- data.tar.gz: 8f3b976cc4b9f8400439a1504bbad9a019865a88a7f41e6556907ba649587c80
3
+ metadata.gz: e6537ec11ece29a487aa35449c264d2bae69089022a28c473f71523e52fd2acd
4
+ data.tar.gz: 231b7285d0ddf23bb686836217ff35d1c89cc3872a5ac4ef20df28cea7e781bb
5
5
  SHA512:
6
- metadata.gz: 90422fd9a948509c2e53b9058d052a822e76f303a736a39488a0870b6fa903d6c1b2b8248e00649269a63ed606a9a8d04b1c2c081cf0f2b89e81000af8a36e54
7
- data.tar.gz: fa76f361560fa078903685d69de81600b8337509b8f37b89cd93fdfdc71ebc3953180edf8487b4aa24d79d521237ef98bc727cde48ca959acb41c0d589749999
6
+ metadata.gz: 31ef9eeacef9208296c2df0d29f5a83e0a2ee8a2278715bd4b67d261fb1561a5b0e11771856b5dfb27d34a28cb21034e7b6cda5408757f6e13672a3db65c8d3d
7
+ data.tar.gz: b686d8bd24fb976e8d9e6842b92db04988b1ae6f5981b297de57fc7d32b0add212d200f5b2906d5a7ea450b47bf46b9373a4404d5449ab24b702b189fb2a9131
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.4] - 2026-07-31
4
+
5
+ Request-completion allocation patch. It replaces the general-purpose
6
+ `Async::Notification` attached to every multiplexed request with a direct
7
+ single-waiter fiber handoff while preserving deferred reactor wakeups and the
8
+ existing SQL, Sync, result and failure semantics.
9
+
10
+ ### Performance
11
+
12
+ - A request now stores only the waiting fiber and its originating scheduler.
13
+ `Scheduler#block` parks that fiber and `Scheduler#unblock` schedules only that
14
+ waiter on a later reactor turn.
15
+ - Removed the per-request `Async::Notification` and its initial
16
+ `Thread::Queue`; when a waiter is present, completion also avoids the
17
+ replacement queue and `Async::Notification::Signal` allocation.
18
+ - The optimization is limited to one-shot request completion. Bounded-queue and
19
+ pinned-pool notifications retain `Async::Notification` because they require
20
+ multi-waiter coordination.
21
+
22
+ ### Reliability
23
+
24
+ - Completion-before-wait still returns immediately through the settled guard, so
25
+ no wakeup can be lost.
26
+ - Timeout or task cancellation clears the stored waiter in an `ensure`, preventing
27
+ a late query completion from retaining or waking an abandoned fiber.
28
+ - A second concurrent waiter is rejected explicitly instead of silently replacing
29
+ the first waiter.
30
+ - The scheduler associated with the waiting fiber is stored and used for the
31
+ matching unblock rather than looking up an implicit current scheduler at
32
+ completion time.
33
+ - Added focused coverage for wait-before-completion, completion after an
34
+ interrupted wait, the single-waiter invariant and already-settled waits outside
35
+ an active scheduler.
36
+
3
37
  ## [0.2.3] - 2026-07-30
4
38
 
5
39
  Performance-focused release based on CPU profiles from the live pipeline
data/DESIGN.md CHANGED
@@ -126,7 +126,7 @@ driver that lacks a successfully registered handle.
126
126
  Registration is explicit: there is no unbounded automatic SQL cache. A failed
127
127
  registration is removed from the reconnect catalog; statements that had already
128
128
  been prepared on another connection may remain there under an unreachable
129
- generated name until that connection is replaced or closed. Version 0.2.3 does
129
+ generated name until that connection is replaced or closed. Version 0.2.3 and later do
130
130
  not expose `DEALLOCATE` for multiplexed handles. Server-side plan invalidation is
131
131
  reported as the ordinary request-local `QueryError`.
132
132
 
@@ -221,15 +221,24 @@ without reset would leak that state to the next caller. After each pinned block:
221
221
 
222
222
  Immediately resuming a waiter from inside the connection owner is unsafe: a
223
223
  resumed caller could re-enter the driver (submit, or close the client and wait on
224
- the owner) while the owner is mid-`signal`. Internal wait/notify points therefore
225
- use `Async::Notification`, which resumes waiters on a later reactor turn, avoiding
226
- reentrancy into the owner. The same rule applies to request completion,
227
- bounded-queue backpressure and pinned-pool idle notifications.
224
+ the owner) while the owner is still completing the current pipeline unit.
225
+
226
+ A `Request` has exactly one consumer, so request completion stores that waiter
227
+ fiber and its scheduler directly. `Scheduler#block` parks it, and
228
+ `Scheduler#unblock` pushes it onto the selector for a later reactor turn. This
229
+ preserves deferred wakeup without allocating a general-purpose
230
+ `Async::Notification` and its queue objects for every query. The settled flag
231
+ handles completion-before-wait, and an `ensure` removes a waiter interrupted by
232
+ timeout or task cancellation before a late result can try to wake it.
233
+
234
+ Multi-waiter coordination points still use `Async::Notification`: bounded-queue
235
+ backpressure and pinned-pool idle notifications need queue/broadcast semantics
236
+ that the request-specific one-shot waiter deliberately does not provide.
228
237
 
229
238
  > Note: the supported Async range starts at 2.42 and stays below 3. The lockfile
230
239
  > exercises the current compatible 2.x release, while CI separately runs the unit
231
- > suite against the exact 2.42.0 floor so this coordination behavior is not merely
232
- > assumed from a broad pessimistic dependency range.
240
+ > suite against the exact 2.42.0 floor so `Scheduler#block`/`#unblock` behavior is
241
+ > not merely assumed from a broad pessimistic dependency range.
233
242
 
234
243
  ## 11. Version policy
235
244
 
data/README.md CHANGED
@@ -187,6 +187,8 @@ See `examples/falcon_config.ru` for a complete setup.
187
187
 
188
188
  ## Benchmark
189
189
 
190
+ [Performance test results and methodology](docs/PERFORMANCE.md).
191
+
190
192
  ```bash
191
193
  docker compose up -d pg17
192
194
  export PG_PIPELINE_URL=postgres://postgres:postgres@127.0.0.1:5417/postgres
@@ -1,26 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "async/notification"
4
-
5
3
  require_relative "errors"
6
4
 
7
5
  module PgPipeline
8
6
  class Request
9
- attr_reader :sql, :params, :condition
7
+ attr_reader :sql, :params
10
8
  attr_accessor :state, :cancelled, :settled, :result_seen, :query_boundary_seen,
11
- :result, :error
9
+ :result, :error, :waiter, :waiter_scheduler
12
10
 
13
11
  def initialize(sql:, params: nil)
14
12
  @sql = RequestOps.snapshot_sql(sql)
15
13
  @params = RequestOps.snapshot_params(params)
16
14
  @state = :new
17
- @condition = Async::Notification.new
18
15
  @cancelled = false
19
16
  @settled = false
20
17
  @result_seen = false
21
18
  @query_boundary_seen = false
22
19
  @result = nil
23
20
  @error = nil
21
+ @waiter = nil
22
+ @waiter_scheduler = nil
24
23
  end
25
24
 
26
25
  def self.prepare(statement) = PrepareRequest.new(statement)
@@ -156,7 +155,7 @@ module PgPipeline
156
155
 
157
156
  req.settled = true
158
157
  req.state = :done
159
- req.condition.signal unless req.cancelled
158
+ wake_waiter(req) unless req.cancelled
160
159
  end
161
160
 
162
161
  def reject!(req, error)
@@ -167,7 +166,7 @@ module PgPipeline
167
166
  req.error ||= error
168
167
  req.settled = true
169
168
  req.state = :done
170
- req.condition.signal unless req.cancelled
169
+ wake_waiter(req) unless req.cancelled
171
170
  end
172
171
 
173
172
  def cancel!(req)
@@ -180,12 +179,42 @@ module PgPipeline
180
179
  end
181
180
 
182
181
  def wait(req)
183
- req.condition.wait unless req.settled
182
+ park_waiter(req) unless req.settled
184
183
  raise req.error if req.error
185
184
 
186
185
  req.result
187
186
  end
188
187
 
188
+ def park_waiter(req)
189
+ raise ProtocolError, "request already has a waiter" if req.waiter
190
+
191
+ scheduler = Fiber.scheduler
192
+ raise Error, "request wait requires an active Fiber scheduler" unless scheduler
193
+
194
+ waiter = Fiber.current
195
+ req.waiter = waiter
196
+ req.waiter_scheduler = scheduler
197
+
198
+ begin
199
+ scheduler.block(req, nil) until req.settled
200
+ ensure
201
+ if req.waiter.equal?(waiter)
202
+ req.waiter = nil
203
+ req.waiter_scheduler = nil
204
+ end
205
+ end
206
+ end
207
+
208
+ def wake_waiter(req)
209
+ waiter = req.waiter
210
+ scheduler = req.waiter_scheduler
211
+
212
+ req.waiter = nil
213
+ req.waiter_scheduler = nil
214
+
215
+ scheduler.unblock(req, waiter) if scheduler && waiter
216
+ end
217
+
189
218
  def assert_result_slot!(req)
190
219
  raise ProtocolError, "result arrived after query boundary" if req.query_boundary_seen
191
220
  raise ProtocolError, "multiple results for one pipeline unit" if req.result_seen
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgPipeline
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_pipeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Hajdarov