pg_pipeline 0.3.1 → 0.3.2
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 +4 -4
- data/CHANGELOG.md +32 -0
- data/lib/pg_pipeline/runtime/notification.rb +12 -7
- data/lib/pg_pipeline/runtime/semaphore.rb +6 -5
- data/lib/pg_pipeline/runtime/task.rb +9 -6
- data/lib/pg_pipeline/runtime.rb +74 -12
- data/lib/pg_pipeline/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7fdda2e5d806be070a3255e8cef4a63b72acaeac87d7a7777d580bfb0fc0d0a
|
|
4
|
+
data.tar.gz: 2b23bf8808241eb64396672959a86ae86c26af05bb0d72049de646907cc9e99d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37a5b065506fb61a675a72023057b0d24254ea7fa411ea358c68e255e0ffabf53079ab3a7214be59b2d09b02063aa2c09875576868a213df8c91526f90834ebd
|
|
7
|
+
data.tar.gz: fb0e3b15f73e8de66b56d09c7791b8e357477c859b83293474ec48c15d41d12298f91c974bd8ebeb0ae2cc6a3437ed598c442e6eb26e8a13787e71a3d35856f7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.2] - 2026-08-17
|
|
4
|
+
|
|
5
|
+
Runtime correctness. Public SQL, Sync-per-unit, failure model, and the
|
|
6
|
+
`Fiber::Scheduler` host contract are unchanged.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Bounded waits (`Notification#wait`, `Task#wait`) park through
|
|
11
|
+
`scheduler.block(blocker, remaining)` and a monotonic deadline. They no
|
|
12
|
+
longer wrap `park` in `Timeout.timeout`. A wait timeout is not a work
|
|
13
|
+
timeout: `Notification#wait` no longer rescues `TimeoutError`, so an
|
|
14
|
+
enclosing `Runtime.with_timeout` cannot be swallowed by
|
|
15
|
+
`BoundedQueue#wait_on` or `wait_for_pinned_idle`. `wait(0)` polls.
|
|
16
|
+
- `Runtime.with_timeout` (health probes and other work limits) calls
|
|
17
|
+
`scheduler.timeout_after(duration, Deadline, message)` when the hook
|
|
18
|
+
exists. `Deadline < Exception`, so `rescue => e` cannot eat it; the
|
|
19
|
+
method still raises `Runtime::TimeoutError` at its own boundary. A
|
|
20
|
+
scheduler without the hook falls back to stdlib `Timeout` and warns
|
|
21
|
+
once — `Thread#raise` can hit the wrong fiber.
|
|
22
|
+
- `Notification#signal` and `Semaphore#release` skip a waiter that
|
|
23
|
+
`Task#stop` already released, or a fiber that is no longer alive. On a
|
|
24
|
+
deferred `unblock` the previous `shift` + wake handed the signal to the
|
|
25
|
+
stopped waiter and left the live one parked; a dead semaphore waiter
|
|
26
|
+
burned a pinned-pool slot. `park` also raises `Cancel` after a stop-wake
|
|
27
|
+
so the wait is not mistaken for a successful signal.
|
|
28
|
+
|
|
29
|
+
### Unchanged
|
|
30
|
+
|
|
31
|
+
- Per-fiber waiter-hash reuse, `Request` one-shot completion, watcher
|
|
32
|
+
`fiber_interrupt` / poll shutdown, and the host-installed scheduler
|
|
33
|
+
rule.
|
|
34
|
+
|
|
3
35
|
## [0.3.1] - 2026-08-09
|
|
4
36
|
|
|
5
37
|
Reliability and correctness fixes on top of 0.3.0. No public API changes other
|
|
@@ -7,19 +7,24 @@ module PgPipeline
|
|
|
7
7
|
@waiters = []
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
def waiting = @waiters.size
|
|
11
|
+
|
|
10
12
|
def wait(timeout = nil)
|
|
13
|
+
wait_until(Runtime.deadline_for(timeout))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def wait_until(deadline = nil)
|
|
11
17
|
Runtime.with_waiter(self, @waiters) do |waiter|
|
|
12
|
-
Runtime.
|
|
13
|
-
true
|
|
14
|
-
rescue TimeoutError
|
|
15
|
-
false
|
|
18
|
+
Runtime.park(self, waiter, deadline) { false }
|
|
16
19
|
end
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def signal
|
|
20
|
-
waiter = @waiters.shift
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
while (waiter = @waiters.shift)
|
|
24
|
+
return true if Runtime.wake_dequeued(waiter, self)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
false
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def signal_all
|
|
@@ -23,12 +23,12 @@ module PgPipeline
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def release
|
|
26
|
-
|
|
26
|
+
while (node = @waiting.shift)
|
|
27
27
|
node.grant!
|
|
28
|
-
node.resume
|
|
29
|
-
else
|
|
30
|
-
@available += 1
|
|
28
|
+
return @available if node.resume
|
|
31
29
|
end
|
|
30
|
+
|
|
31
|
+
@available += 1
|
|
32
32
|
@available
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -126,9 +126,10 @@ module PgPipeline
|
|
|
126
126
|
|
|
127
127
|
def resume
|
|
128
128
|
fiber = @fiber
|
|
129
|
-
return unless fiber.alive?
|
|
129
|
+
return false unless fiber.alive?
|
|
130
130
|
|
|
131
131
|
@scheduler.unblock(@blocker, fiber)
|
|
132
|
+
true
|
|
132
133
|
end
|
|
133
134
|
end
|
|
134
135
|
|
|
@@ -22,7 +22,10 @@ module PgPipeline
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def wait(timeout = nil)
|
|
25
|
-
|
|
25
|
+
unless @done
|
|
26
|
+
deadline = Runtime.deadline_for(timeout)
|
|
27
|
+
raise TimeoutError, "task did not finish in time" unless join(deadline)
|
|
28
|
+
end
|
|
26
29
|
raise @error if @error
|
|
27
30
|
|
|
28
31
|
@result
|
|
@@ -94,8 +97,8 @@ module PgPipeline
|
|
|
94
97
|
|
|
95
98
|
def release_blocker
|
|
96
99
|
waiter = @blocker or return false
|
|
97
|
-
|
|
98
|
-
waiter[:fiber].alive?
|
|
100
|
+
|
|
101
|
+
Runtime.wake(waiter, waiter[:blocker]) && waiter[:fiber].alive?
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def complete(result, error)
|
|
@@ -107,11 +110,11 @@ module PgPipeline
|
|
|
107
110
|
wake_waiters
|
|
108
111
|
end
|
|
109
112
|
|
|
110
|
-
def join
|
|
111
|
-
return if @done
|
|
113
|
+
def join(deadline)
|
|
114
|
+
return true if @done
|
|
112
115
|
|
|
113
116
|
Runtime.with_waiter(self, @waiters) do |waiter|
|
|
114
|
-
Runtime.park(self, waiter) { @done }
|
|
117
|
+
Runtime.park(self, waiter, deadline) { @done }
|
|
115
118
|
end
|
|
116
119
|
end
|
|
117
120
|
|
data/lib/pg_pipeline/runtime.rb
CHANGED
|
@@ -7,12 +7,25 @@ require_relative "errors"
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
module Runtime
|
|
9
9
|
class Cancel < Exception; end
|
|
10
|
+
class Deadline < Exception; end
|
|
10
11
|
class TimeoutError < Error; end
|
|
11
12
|
|
|
12
13
|
module_function
|
|
13
14
|
|
|
14
15
|
CURRENT_TASK_KEY = :pg_pipeline_current_task
|
|
15
16
|
WAITER_KEY = :pg_pipeline_waiter
|
|
17
|
+
DEADLINE_MESSAGE = "execution expired"
|
|
18
|
+
|
|
19
|
+
MISSING_TIMEOUT_HOOK_WARNING = <<~MESSAGE
|
|
20
|
+
pg_pipeline: %s does not implement #timeout_after.
|
|
21
|
+
|
|
22
|
+
Falling back to stdlib Timeout, which uses Thread#raise and can deliver
|
|
23
|
+
the timeout to an unrelated fiber. Use a scheduler that implements
|
|
24
|
+
#timeout_after (async, itsi-scheduler) or avoid Runtime.with_timeout
|
|
25
|
+
on this host.
|
|
26
|
+
MESSAGE
|
|
27
|
+
|
|
28
|
+
@warned_schedulers = {}
|
|
16
29
|
|
|
17
30
|
def spawn(name: nil, &block)
|
|
18
31
|
Task.spawn(name: name, &block)
|
|
@@ -22,6 +35,10 @@ module PgPipeline
|
|
|
22
35
|
Fiber.scheduler or raise Error, "operation requires an active Fiber scheduler"
|
|
23
36
|
end
|
|
24
37
|
|
|
38
|
+
def native_timeouts?(target = Fiber.scheduler)
|
|
39
|
+
!target.nil? && target.respond_to?(:timeout_after)
|
|
40
|
+
end
|
|
41
|
+
|
|
25
42
|
def current_task
|
|
26
43
|
Thread.current[CURRENT_TASK_KEY]
|
|
27
44
|
end
|
|
@@ -44,16 +61,20 @@ module PgPipeline
|
|
|
44
61
|
waiter
|
|
45
62
|
end
|
|
46
63
|
|
|
47
|
-
def park(blocker, waiter)
|
|
64
|
+
def park(blocker, waiter, deadline = nil)
|
|
48
65
|
scheduler = waiter[:scheduler]
|
|
49
66
|
task = current_task
|
|
50
67
|
task&.enter_block(waiter)
|
|
51
68
|
|
|
52
69
|
until waiter[:ready] || yield
|
|
53
|
-
|
|
70
|
+
remaining = deadline ? (deadline - monotonic_now) : nil
|
|
71
|
+
return false if deadline && remaining <= 0
|
|
72
|
+
|
|
73
|
+
scheduler.block(blocker, remaining)
|
|
54
74
|
task&.raise_if_cancelled!
|
|
55
75
|
end
|
|
56
|
-
|
|
76
|
+
task&.raise_if_cancelled!
|
|
77
|
+
true
|
|
57
78
|
ensure
|
|
58
79
|
waiter[:blocker] = nil
|
|
59
80
|
task&.exit_block
|
|
@@ -65,7 +86,10 @@ module PgPipeline
|
|
|
65
86
|
waiters << waiter
|
|
66
87
|
yield waiter
|
|
67
88
|
ensure
|
|
68
|
-
|
|
89
|
+
if waiter[:queued]
|
|
90
|
+
waiters.delete(waiter)
|
|
91
|
+
waiter[:queued] = false
|
|
92
|
+
end
|
|
69
93
|
end
|
|
70
94
|
|
|
71
95
|
def wake_dequeued(waiter, blocker)
|
|
@@ -73,21 +97,59 @@ module PgPipeline
|
|
|
73
97
|
wake(waiter, blocker)
|
|
74
98
|
end
|
|
75
99
|
|
|
100
|
+
def wake(waiter, blocker)
|
|
101
|
+
return false if waiter[:ready]
|
|
102
|
+
|
|
103
|
+
waiter[:ready] = true
|
|
104
|
+
fiber = waiter[:fiber]
|
|
105
|
+
return false unless fiber.alive?
|
|
106
|
+
|
|
107
|
+
waiter[:scheduler].unblock(blocker, fiber)
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def monotonic_now
|
|
112
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def deadline_for(timeout)
|
|
116
|
+
return nil if timeout.nil?
|
|
117
|
+
|
|
118
|
+
seconds = Float(timeout)
|
|
119
|
+
raise ArgumentError, "timeout must be non-negative and finite" unless seconds.finite? && seconds >= 0
|
|
120
|
+
|
|
121
|
+
monotonic_now + seconds
|
|
122
|
+
end
|
|
123
|
+
|
|
76
124
|
def with_timeout(duration)
|
|
77
125
|
return yield if duration.nil?
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
raise ArgumentError, "timeout must be non-negative and finite" unless
|
|
127
|
+
seconds = Float(duration)
|
|
128
|
+
raise ArgumentError, "timeout must be non-negative and finite" unless seconds.finite? && seconds >= 0
|
|
81
129
|
|
|
82
|
-
|
|
130
|
+
begin
|
|
131
|
+
arm_deadline(seconds) { yield }
|
|
132
|
+
rescue Deadline => e
|
|
133
|
+
raise TimeoutError, e.message
|
|
134
|
+
end
|
|
83
135
|
end
|
|
84
136
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
137
|
+
def arm_deadline(seconds, &block)
|
|
138
|
+
target = Fiber.scheduler
|
|
139
|
+
if native_timeouts?(target)
|
|
140
|
+
return target.timeout_after(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
141
|
+
end
|
|
89
142
|
|
|
90
|
-
|
|
143
|
+
warn_missing_timeout_hook(target) if target
|
|
144
|
+
::Timeout.timeout(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def warn_missing_timeout_hook(target)
|
|
148
|
+
key = target.class
|
|
149
|
+
return if @warned_schedulers[key]
|
|
150
|
+
|
|
151
|
+
@warned_schedulers[key] = true
|
|
152
|
+
warn(format(MISSING_TIMEOUT_HOOK_WARNING, key))
|
|
91
153
|
end
|
|
92
154
|
end
|
|
93
155
|
end
|
data/lib/pg_pipeline/version.rb
CHANGED