pg_pipeline 0.2.5 → 0.3.1

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.
@@ -255,13 +255,11 @@ module PgPipeline
255
255
  req.waiter = waiter
256
256
  req.waiter_scheduler = scheduler
257
257
 
258
- begin
259
- scheduler.block(req, nil) until req.settled
260
- ensure
261
- if req.waiter.equal?(waiter)
262
- req.waiter = nil
263
- req.waiter_scheduler = nil
264
- end
258
+ scheduler.block(req, nil) until req.settled
259
+ ensure
260
+ if waiter && req.waiter.equal?(waiter)
261
+ req.waiter = nil
262
+ req.waiter_scheduler = nil
265
263
  end
266
264
  end
267
265
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgPipeline
4
+ module Runtime
5
+ class Notification
6
+ def initialize
7
+ @waiters = []
8
+ end
9
+
10
+ def wait(timeout = nil)
11
+ Runtime.with_waiter(self, @waiters) do |waiter|
12
+ Runtime.with_timeout(timeout) { Runtime.park(self, waiter) { false } }
13
+ true
14
+ rescue TimeoutError
15
+ false
16
+ end
17
+ end
18
+
19
+ def signal
20
+ waiter = @waiters.shift or return false
21
+ Runtime.wake_dequeued(waiter, self)
22
+ true
23
+ end
24
+
25
+ def signal_all
26
+ pending, @waiters = @waiters, []
27
+ pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgPipeline
4
+ module Runtime
5
+ class Queue
6
+ def initialize
7
+ @queue = ::Thread::Queue.new
8
+ end
9
+
10
+ def enqueue(item)
11
+ @queue.push(item)
12
+ item
13
+ rescue ClosedQueueError
14
+ nil
15
+ end
16
+
17
+ def dequeue
18
+ @queue.pop
19
+ end
20
+
21
+ def close
22
+ @queue.close
23
+ nil
24
+ end
25
+
26
+ def closed? = @queue.closed?
27
+ def empty? = @queue.empty?
28
+ def size = @queue.size
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgPipeline
4
+ module Runtime
5
+ class Semaphore
6
+ def initialize(limit)
7
+ @limit = Integer(limit)
8
+ raise ArgumentError, "limit must be >= 0" if @limit.negative?
9
+
10
+ @available = @limit
11
+ @waiting = WaitList.new
12
+ end
13
+
14
+ def acquire
15
+ wait
16
+ return @available unless block_given?
17
+
18
+ begin
19
+ yield
20
+ ensure
21
+ release
22
+ end
23
+ end
24
+
25
+ def release
26
+ if (node = @waiting.shift)
27
+ node.grant!
28
+ node.resume
29
+ else
30
+ @available += 1
31
+ end
32
+ @available
33
+ end
34
+
35
+ private
36
+
37
+ def wait
38
+ return (@available -= 1) if @available.positive?
39
+
40
+ task = Runtime.current_task
41
+ node = FiberNode.new(Fiber.current, self)
42
+ @waiting.push(node)
43
+ task&.enter_block(node.waiter)
44
+
45
+ begin
46
+ until node.granted?
47
+ node.suspend
48
+ abandon_wait!(node, task) if task&.cancelled?
49
+ end
50
+ abandon_wait!(node, task) if task&.cancelled?
51
+ ensure
52
+ task&.exit_block
53
+ @waiting.remove(node) unless node.granted? || node.list.nil?
54
+ end
55
+ end
56
+
57
+ def abandon_wait!(node, task)
58
+ node.granted? ? release : @waiting.remove(node)
59
+ task.raise_if_cancelled!
60
+ end
61
+
62
+ class WaitList
63
+ attr_reader :size
64
+
65
+ def initialize
66
+ @head = @tail = nil
67
+ @size = 0
68
+ end
69
+
70
+ def first = @head
71
+ def empty? = @head.nil?
72
+
73
+ def push(node)
74
+ raise ArgumentError, "node already queued" if node.list
75
+
76
+ node.list = self
77
+ node.prev = @tail
78
+ node.next = nil
79
+ @tail ? (@tail.next = node) : (@head = node)
80
+ @tail = node
81
+ @size += 1
82
+ node
83
+ end
84
+
85
+ def shift
86
+ node = @head or return nil
87
+ remove(node)
88
+ end
89
+
90
+ def remove(node)
91
+ return nil unless node.list.equal?(self)
92
+
93
+ node.prev ? (node.prev.next = node.next) : (@head = node.next)
94
+ node.next ? (node.next.prev = node.prev) : (@tail = node.prev)
95
+ node.list = node.prev = node.next = nil
96
+ @size -= 1
97
+ node
98
+ end
99
+ end
100
+
101
+ class FiberNode
102
+ attr_accessor :list, :prev, :next
103
+ attr_reader :fiber, :waiter, :blocker
104
+
105
+ def initialize(fiber, blocker)
106
+ @fiber = fiber
107
+ @blocker = blocker
108
+ @list = @prev = @next = nil
109
+ @granted = false
110
+ @scheduler = Fiber.scheduler
111
+ @waiter = {fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true}
112
+ end
113
+
114
+ def granted? = @granted
115
+
116
+ def grant!
117
+ @granted = true
118
+ @waiter[:ready] = true
119
+ @waiter[:queued] = false
120
+ self
121
+ end
122
+
123
+ def suspend
124
+ @scheduler.block(@blocker, nil)
125
+ end
126
+
127
+ def resume
128
+ fiber = @fiber
129
+ return unless fiber.alive?
130
+
131
+ @scheduler.unblock(@blocker, fiber)
132
+ end
133
+ end
134
+
135
+ private_constant :WaitList, :FiberNode
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgPipeline
4
+ module Runtime
5
+ class Task
6
+ attr_reader :fiber, :name, :error
7
+
8
+ def self.spawn(name: nil, &block)
9
+ raise ArgumentError, "block required" unless block
10
+
11
+ task = new(name: name)
12
+ task.__send__(:start, &block)
13
+ task
14
+ end
15
+
16
+ def initialize(name: nil)
17
+ @fiber, @scheduler, @blocker, @result, @error = nil, nil, nil, nil, nil
18
+ @done, @cancelled = false, false
19
+
20
+ @name = name
21
+ @waiters = []
22
+ end
23
+
24
+ def wait(timeout = nil)
25
+ Runtime.with_timeout(timeout) { join }
26
+ raise @error if @error
27
+
28
+ @result
29
+ end
30
+
31
+ def stop
32
+ return false if @done
33
+
34
+ @cancelled = true
35
+
36
+ return true if interrupt_fiber
37
+ return true if release_blocker
38
+
39
+ false
40
+ end
41
+
42
+ def cancelled? = @cancelled
43
+
44
+ def raise_if_cancelled!
45
+ raise Cancel, "task stopped" if @cancelled
46
+ end
47
+
48
+ def enter_block(waiter)
49
+ @blocker = waiter
50
+ end
51
+
52
+ def exit_block
53
+ @blocker = nil
54
+ end
55
+
56
+ def finished? = @done
57
+
58
+ private
59
+
60
+ def start(&block)
61
+ @scheduler = Runtime.scheduler!
62
+ this = self
63
+
64
+ scheduled = Fiber.schedule do
65
+ this.__send__(:adopt_fiber, Fiber.current)
66
+ Thread.current[Runtime::CURRENT_TASK_KEY] = this
67
+ begin
68
+ this.__send__(:complete, block.call, nil)
69
+ rescue Cancel
70
+ this.__send__(:complete, nil, nil)
71
+ rescue Exception => e
72
+ this.__send__(:complete, nil, e)
73
+ end
74
+ end
75
+
76
+ @fiber ||= scheduled
77
+ end
78
+
79
+ def adopt_fiber(fiber)
80
+ @fiber ||= fiber
81
+ end
82
+
83
+ def interrupt_fiber
84
+ fiber = @fiber
85
+ scheduler = @scheduler
86
+ return false unless fiber&.alive?
87
+ return false unless scheduler&.respond_to?(:fiber_interrupt)
88
+
89
+ result = scheduler.fiber_interrupt(fiber, Cancel.new("task stopped"))
90
+ result != false
91
+ rescue FiberError
92
+ false
93
+ end
94
+
95
+ def release_blocker
96
+ waiter = @blocker or return false
97
+ Runtime.wake(waiter, waiter[:blocker])
98
+ waiter[:fiber].alive?
99
+ end
100
+
101
+ def complete(result, error)
102
+ return if @done
103
+
104
+ @result = result
105
+ @error = error
106
+ @done = true
107
+ wake_waiters
108
+ end
109
+
110
+ def join
111
+ return if @done
112
+
113
+ Runtime.with_waiter(self, @waiters) do |waiter|
114
+ Runtime.park(self, waiter) { @done }
115
+ end
116
+ end
117
+
118
+ def wake_waiters
119
+ pending, @waiters = @waiters, []
120
+ pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ require_relative "errors"
6
+
7
+ module PgPipeline
8
+ module Runtime
9
+ class Cancel < Exception; end
10
+ class TimeoutError < Error; end
11
+
12
+ module_function
13
+
14
+ CURRENT_TASK_KEY = :pg_pipeline_current_task
15
+ WAITER_KEY = :pg_pipeline_waiter
16
+
17
+ def spawn(name: nil, &block)
18
+ Task.spawn(name: name, &block)
19
+ end
20
+
21
+ def scheduler!
22
+ Fiber.scheduler or raise Error, "operation requires an active Fiber scheduler"
23
+ end
24
+
25
+ def current_task
26
+ Thread.current[CURRENT_TASK_KEY]
27
+ end
28
+
29
+ def build_waiter(blocker)
30
+ waiter = Thread.current[WAITER_KEY] ||= {
31
+ fiber: nil, scheduler: nil, ready: false, blocker: nil
32
+ }
33
+
34
+ if waiter[:blocker]
35
+ raise Error, "waiter already parked on #{waiter[:blocker].class}; " \
36
+ "a fiber may only park in one place at a time"
37
+ end
38
+
39
+ waiter[:fiber] = Fiber.current
40
+ waiter[:scheduler] = scheduler!
41
+ waiter[:ready] = false
42
+ waiter[:queued] = false
43
+ waiter[:blocker] = blocker
44
+ waiter
45
+ end
46
+
47
+ def park(blocker, waiter)
48
+ scheduler = waiter[:scheduler]
49
+ task = current_task
50
+ task&.enter_block(waiter)
51
+
52
+ until waiter[:ready] || yield
53
+ scheduler.block(blocker, nil)
54
+ task&.raise_if_cancelled!
55
+ end
56
+ nil
57
+ ensure
58
+ waiter[:blocker] = nil
59
+ task&.exit_block
60
+ end
61
+
62
+ def with_waiter(blocker, waiters)
63
+ waiter = build_waiter(blocker)
64
+ waiter[:queued] = true
65
+ waiters << waiter
66
+ yield waiter
67
+ ensure
68
+ waiters.delete(waiter) if waiter[:queued]
69
+ end
70
+
71
+ def wake_dequeued(waiter, blocker)
72
+ waiter[:queued] = false
73
+ wake(waiter, blocker)
74
+ end
75
+
76
+ def with_timeout(duration)
77
+ return yield if duration.nil?
78
+
79
+ timeout = Float(duration)
80
+ raise ArgumentError, "timeout must be non-negative and finite" unless timeout.finite? && timeout >= 0
81
+
82
+ ::Timeout.timeout(timeout, TimeoutError) { yield }
83
+ end
84
+
85
+ def wake(waiter, blocker)
86
+ waiter[:ready] = true
87
+ fiber = waiter[:fiber]
88
+ return unless fiber.alive?
89
+
90
+ waiter[:scheduler].unblock(blocker, fiber)
91
+ end
92
+ end
93
+ end
94
+
95
+ require_relative "runtime/notification"
96
+ require_relative "runtime/queue"
97
+ require_relative "runtime/semaphore"
98
+ require_relative "runtime/task"
@@ -18,7 +18,6 @@ module PgPipeline
18
18
  "select-into-temp" => /\binto\s+(?:(?:global|local)\s+)?temp(?:orary)?\b/i,
19
19
  "select-into-pg-temp" => /\binto\s+(?:table\s+)?pg_temp(?:_\d+)?\s*\./i
20
20
  }.freeze
21
-
22
21
  STRICT_FORBIDDEN = {
23
22
  "nextval" => /\bnextval\s*\(/i,
24
23
  "setval" => /\bsetval\s*\(/i,
@@ -28,9 +27,9 @@ module PgPipeline
28
27
  "session-advisory-unlock" => /\bpg_advisory_unlock(?:_shared|_all)?\s*\(/i,
29
28
  "pg_export_snapshot" => /\bpg_export_snapshot\s*\(/i
30
29
  }.freeze
31
-
32
30
  PATTERN_PREFILTER = /\binto\b/i
33
- NEEDS_MASK = /['"]|--|\/\*|\$[A-Za-z_0-9]*\$/
31
+ DOLLAR_QUOTE_TAG = /\A\$(?:(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_]|[^\x00-\x7F])*)?\$/
32
+ NEEDS_MASK = /['"]|--|\/\*|\$(?:(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_]|[^\x00-\x7F])*)?\$/
34
33
  LEADING_KEYWORD = /\A\s*([a-zA-Z_]+)/
35
34
  WHITESPACE_BYTES = [9, 10, 11, 12, 13, 32].freeze
36
35
 
@@ -84,7 +83,6 @@ module PgPipeline
84
83
  FORBIDDEN_PATTERNS.each do |name, pattern|
85
84
  return name if code.match?(pattern)
86
85
  end
87
-
88
86
  if mode == :strict
89
87
  STRICT_FORBIDDEN.each do |name, pattern|
90
88
  return "strict:#{name}" if code.match?(pattern)
@@ -111,7 +109,6 @@ module PgPipeline
111
109
  while index < size
112
110
  byte = source.getbyte(index)
113
111
  nxt = index + 1 < size ? source.getbyte(index + 1) : nil
114
-
115
112
  if block_depth.positive?
116
113
  if byte == 47 && nxt == 42
117
114
  block_depth += 1
@@ -159,7 +156,7 @@ module PgPipeline
159
156
 
160
157
  if byte == 36
161
158
  remainder = source.byteslice(index, size - index)
162
- tag = remainder.match(/\A\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)&.[](0)
159
+ tag = DOLLAR_QUOTE_TAG.match(remainder)&.[](0)
163
160
  if tag
164
161
  closing = source.index(tag, index + tag.bytesize)
165
162
  finish = closing ? closing + tag.bytesize : size
@@ -182,7 +179,6 @@ module PgPipeline
182
179
 
183
180
  while index < source.bytesize
184
181
  byte = source.getbyte(index)
185
-
186
182
  if escape_backslash && byte == 92
187
183
  output << " "
188
184
  index += 1
@@ -203,7 +199,6 @@ module PgPipeline
203
199
  index += 1
204
200
  end
205
201
  end
206
-
207
202
  index
208
203
  end
209
204
  private_class_method :mask_quoted
@@ -39,7 +39,7 @@ module PgPipeline
39
39
  tx.__send__(:open=, true)
40
40
  begin
41
41
  result = yield tx
42
- conn.exec("COMMIT")
42
+ commit(conn)
43
43
  tx.__send__(:open=, false)
44
44
  result
45
45
  rescue Exception
@@ -48,6 +48,28 @@ module PgPipeline
48
48
  end
49
49
  end
50
50
 
51
+ def commit(conn)
52
+ conn.exec("COMMIT")
53
+ rescue PG::Error => error
54
+ raise unless indeterminate_commit_failure?(conn, error)
55
+
56
+ raise IndeterminateCommitError.new(
57
+ "COMMIT acknowledgement was not received; the transaction may have committed " \
58
+ "and must not be retried blindly (#{error.class}: #{error.message})"
59
+ ), cause: error
60
+ end
61
+
62
+ def indeterminate_commit_failure?(conn, error)
63
+ return true if defined?(PG::ConnectionBad) && error.is_a?(PG::ConnectionBad)
64
+ return true if conn.finished?
65
+
66
+ conn.status != PG::CONNECTION_OK
67
+ rescue PG::Error
68
+ true
69
+ rescue NoMethodError
70
+ false
71
+ end
72
+
51
73
  def savepoint(tx, name)
52
74
  SessionOps.ensure_active!(tx)
53
75
  raise Error, "savepoint requires an open transaction" unless tx.open?
@@ -57,7 +79,6 @@ module PgPipeline
57
79
  tx.__send__(:savepoint_seq=, seq)
58
80
  point = name || "pgp_sp_#{seq}"
59
81
  ident = conn.quote_ident(point)
60
-
61
82
  conn.exec("SAVEPOINT #{ident}")
62
83
  begin
63
84
  result = yield tx
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgPipeline
4
- VERSION = "0.2.5"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/pg_pipeline.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "pg_pipeline/version"
4
4
  require_relative "pg_pipeline/errors"
5
+ require_relative "pg_pipeline/runtime"
5
6
  require_relative "pg_pipeline/server_caps"
6
7
  require_relative "pg_pipeline/session_guard"
7
8
  require_relative "pg_pipeline/session"
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.5
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Hajdarov
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: async
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '2.42'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '2.42'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: pg
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -43,6 +29,20 @@ dependencies:
43
29
  - - "<"
44
30
  - !ruby/object:Gem::Version
45
31
  version: '2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: async
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '2.42'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.42'
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: rake
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -101,10 +101,12 @@ dependencies:
101
101
  version: '0.2'
102
102
  description: |
103
103
  A driver-adjacent Ruby control-plane over ruby-pg/libpq. It multiplexes
104
- independent, session-neutral extended-protocol operations from many Async
105
- fibers onto a small number of PostgreSQL connections while keeping explicit
104
+ independent, session-neutral extended-protocol operations from many fibers
105
+ onto a small number of PostgreSQL connections while keeping explicit
106
106
  transactions and session-changing work on exclusive pinned connections.
107
- Control-plane only: all wire work stays in libpq. Zero lines of C.
107
+ Requires any Fiber::Scheduler host (Async::Scheduler, Itsi::Scheduler, …);
108
+ the gem does not depend on a particular reactor. Control-plane only: all
109
+ wire work stays in libpq.
108
110
  email:
109
111
  - romanhajdarov@gmail.com
110
112
  executables: []
@@ -123,6 +125,11 @@ files:
123
125
  - lib/pg_pipeline/pool.rb
124
126
  - lib/pg_pipeline/prepared_statement.rb
125
127
  - lib/pg_pipeline/request.rb
128
+ - lib/pg_pipeline/runtime.rb
129
+ - lib/pg_pipeline/runtime/notification.rb
130
+ - lib/pg_pipeline/runtime/queue.rb
131
+ - lib/pg_pipeline/runtime/semaphore.rb
132
+ - lib/pg_pipeline/runtime/task.rb
126
133
  - lib/pg_pipeline/server_caps.rb
127
134
  - lib/pg_pipeline/session.rb
128
135
  - lib/pg_pipeline/session_guard.rb
@@ -152,5 +159,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
159
  requirements: []
153
160
  rubygems_version: 3.6.7
154
161
  specification_version: 4
155
- summary: Async-native PostgreSQL pipeline multiplexing on top of ruby-pg
162
+ summary: Fiber-scheduler PostgreSQL pipeline multiplexing on top of ruby-pg
156
163
  test_files: []