concurrent_monitor 0.0.1.ci.release → 0.9.0.rc1

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: 0fb40509148c10d59f1faf6655bd3bb5caf6bc2afc17dc7a3e63b7bfae6d04d1
4
- data.tar.gz: 071b45c7a8e404b2d8fa6c175355ae8066bdc8693cdff951fce12d07dc2865bd
3
+ metadata.gz: 6d2a561842bcf407eb933762b7d48f9033b7b4c7d45dc6c042055b689b1ed6b8
4
+ data.tar.gz: f2de6f5698c4392c16c089cd9556e3942ab947a9459942d4e3757d2872963a18
5
5
  SHA512:
6
- metadata.gz: 9ec7055d648bb1f10c4f74e28fc0b4d7f2ddbdad9df2851b88ee2f5af2c7cc60cf12ddbd2235e3e45eaadbc78b221bc5a693ec76a44f411866e4b96333c6c025
7
- data.tar.gz: 10717aee9fa99a78e800e53bf258e0634e4c7ca81d6f399ebdcecd62701348fb4847c77b14ce340a89076f979c0289eee969348bd3cf7a9d07aae210a4a576f0
6
+ metadata.gz: 17403d800fd04ce45099243c267027755f18d24b7bb84f9fdd798c3bba4dab384978d40a76df8dba397b14978207d00a6e9c3d031bbb6dc286a81234c1a6f407
7
+ data.tar.gz: 68fff665dfa471b808bafbdf4b27fe6d423ea9a5ad0405330520f21a1fff6c3f3dbd8410a91a34c5f61ca4e77aa0da3def2081c7bb9c18f9c6f542182ed58ab4
data/lib/async/monitor.rb CHANGED
@@ -10,12 +10,16 @@ module Async
10
10
  class Monitor
11
11
  # Common task interface over Async::Task
12
12
  class Task < ConcurrentMonitor::Task
13
- def initialize(name = nil, report_on_exception: true, &block)
13
+ def initialize(name_arg = nil, name: name_arg, report_on_exception: true, &block)
14
14
  super()
15
- Async(annotation: name, finished: report_on_exception ? nil : Async::Condition.new) do |task|
16
- @task = task
17
- Fiber.current.concurrent_monitor_task = self
18
- block.call(self)
15
+ if block_given?
16
+ Async(annotation: name, finished: report_on_exception ? nil : false) do |task|
17
+ @task = task
18
+ Fiber.current.concurrent_monitor_task = self
19
+ block.call(self)
20
+ end
21
+ else
22
+ @task = Async::Task.current
19
23
  end
20
24
  end
21
25
 
@@ -23,7 +27,12 @@ module Async
23
27
 
24
28
  def current? = @task.current?
25
29
 
26
- def value = @task.wait
30
+ def value
31
+ result = @task.wait
32
+ raise ConcurrentMonitor::TaskStopped, 'Task was stopped' if @task.stopped?
33
+
34
+ result
35
+ end
27
36
 
28
37
  def stop
29
38
  raise Async::Stop if current?
@@ -56,7 +65,7 @@ module Async
56
65
  end
57
66
 
58
67
  def current_task
59
- Fiber.current.concurrent_monitor_task
68
+ Fiber.current.concurrent_monitor_task ||= Task.new
60
69
  end
61
70
 
62
71
  def new_monitor
@@ -43,7 +43,7 @@ module ConcurrentMonitor
43
43
  # @param [:to_s] name
44
44
  # @param [Boolean] report_on_exception
45
45
  # @return [Task]
46
- def async(name = nil, report_on_exception: false, &)
46
+ def async(name_arg = nil, name: name_arg, report_on_exception: false, &)
47
47
  synchronize { monitor.async(name, report_on_exception:) { |t| run_task(t, &) }.tap { |t| tasks << t } }
48
48
  end
49
49
 
@@ -74,8 +74,9 @@ module ConcurrentMonitor
74
74
  return enum_for(:each).lazy unless block_given?
75
75
 
76
76
  each_task do |t|
77
- v = t.value
78
- yield v unless t.stopped?
77
+ yield t.value
78
+ rescue TaskStopped
79
+ # skip stopped tasks
79
80
  end
80
81
  end
81
82
 
@@ -96,6 +97,11 @@ module ConcurrentMonitor
96
97
  (block_given? ? yield(self) : self).tap { each(&:itself) }
97
98
  end
98
99
 
100
+ # The value of a barrier is the array of values of all its tasks.
101
+ def value
102
+ to_a
103
+ end
104
+
99
105
  # {#wait}, ensuring {#stop}
100
106
  def wait!(&) = ensure_stop { wait(&) }
101
107
 
@@ -11,7 +11,8 @@ module ConcurrentMonitor
11
11
  @error = nil
12
12
  end
13
13
 
14
- # Blocks until the future is completed or the timeout expires
14
+ # Blocks until the future is completed, then returns its value or raises its error.
15
+ #
15
16
  # @return [Object] The value with which the future was fulfilled
16
17
  # @raise [StandardError] If the future was rejected
17
18
  def value
@@ -21,8 +22,13 @@ module ConcurrentMonitor
21
22
  @value
22
23
  end
23
24
 
24
- def wait(timeout = nil, **wait_opts)
25
- synchronize { condition.wait_until(timeout, **wait_opts) { @completed } }
25
+ # Wait until completed
26
+ # @overload wait(timeout, exception: nil)
27
+ # @param timeout [Numeric|nil]
28
+ # @param exception [Class<StandardError>]an error to raise if timeout
29
+ # @return [Boolean] true if completed, nil or raises exception if timed out
30
+ def wait(timeout_arg = nil, timeout: timeout_arg, exception: nil)
31
+ synchronize { condition.wait_until(timeout, exception:) { @completed } }
26
32
  end
27
33
 
28
34
  # Resolve the future with a block
@@ -41,6 +47,11 @@ module ConcurrentMonitor
41
47
  complete! { @value = value }
42
48
  end
43
49
 
50
+ # @return [Boolean] true if completed with a value, otherwise false
51
+ def fulfilled?
52
+ completed? && !@error
53
+ end
54
+
44
55
  # Rejects this future with the given error
45
56
  # @param error [Exception] The error to reject this future with
46
57
  # @return [Boolean] true if the future was rejected, false if already completed
@@ -49,10 +60,17 @@ module ConcurrentMonitor
49
60
  complete! { @error = error }
50
61
  end
51
62
 
63
+ # @return [Boolean] false if not complete or completed with a value
64
+ # @return [StandardError] if completed with an error
65
+ def rejected?
66
+ completed? && @error
67
+ end
68
+
52
69
  # @return [Boolean] true if this future has been completed
53
70
  def completed?
54
71
  synchronize { @completed }
55
72
  end
73
+ alias resolved? completed?
56
74
 
57
75
  # @return [Boolean] true if this future has not yet been completed
58
76
  def pending?
@@ -37,7 +37,7 @@ module ConcurrentMonitor
37
37
  end
38
38
 
39
39
  # Start a task, blocking until the semaphore can be acquired
40
- def async(name = nil, report_on_exception: true, &)
40
+ def async(name_arg = nil, name: name_arg, report_on_exception: true, &)
41
41
  synchronize do
42
42
  @condition.wait_while { @task_count >= @limit }
43
43
  @task_count += 1
@@ -3,32 +3,53 @@
3
3
  require_relative 'wait_timeout'
4
4
 
5
5
  module ConcurrentMonitor
6
+ # Raised when calling #value on a stopped task
7
+ class TaskStopped < StandardError; end
8
+
6
9
  # @abstract
7
10
  # Common interface to underlying tasks
8
11
  class Task
9
12
  # @!method value
10
- # Wait for task to complete and return its value
13
+ # Wait for task to complete and return its value, or raise its error
11
14
  # @return [Object]
15
+ # @raise [TaskStopped] if the task was explicitly {#stop stopped} (Thread API)
12
16
  # @raise [StandardError]
13
17
 
14
- # Wait for task to complete
15
- # @return [self]
18
+ # Wait for task to complete and return its value, or raise its error
19
+ # @return [Object]
20
+ # @return [nil] if the task was explicitly {#stop stopped} (Async::Task API)
21
+ # @raise [StandardError] if task raised an exception
22
+ # @see stopped?
16
23
  def wait
17
24
  value
18
- self
25
+ rescue TaskStopped
26
+ nil
19
27
  end
20
28
 
21
- alias join wait
29
+ # Wait for task to complete and return self (Thread API)
30
+ # @return [self]
31
+ # @raise [TaskStopped] if task was stopped
32
+ # @raise [StandardError] if task raised an exception
33
+ def join
34
+ value
35
+ self
36
+ end
22
37
 
23
38
  # @!method stop
24
39
  # @return [self] after stopping
25
40
  # @raise [Exception] if called from the current task
26
41
 
27
42
  # @!method stopped?
43
+ # Check if task was stopped.
44
+ #
45
+ # A false result is ambiguous unless the calling task has called {#wait} (or rescued {TaskStopped} from a call to
46
+ # {#value}/{#join})
28
47
  # @return [Boolean] true if the task has completed via stop.
29
48
 
30
49
  # @!method alive?
31
- # @return [Boolean] true if the task has not reached completion (as seen by the calling task)
50
+ # A true result is ambiguous unless the calling task has called {#wait} (or rescued {TaskStopped} from a call to
51
+ # {#value}/{#join})
52
+ # @return [Boolean] true if the task has not reached completion
32
53
 
33
54
  # @!method current?
34
55
  # @return [Boolean] true if this task is the current thread/fiber
@@ -23,12 +23,12 @@ module ConcurrentMonitor
23
23
  # Create a TimeoutClock and wait until block is true. See {#wait_until}
24
24
  # @example
25
25
  # TimeoutClock.wait_until(60) { closed? || (sleep(1) && false)}
26
- def wait_until(timeout = nil, delay: nil, exception: nil, &)
26
+ def wait_until(timeout_arg = nil, timeout: timeout_arg, delay: nil, exception: nil, &)
27
27
  timeout(timeout).wait_until(exception:, delay:, &)
28
28
  end
29
29
 
30
30
  # Create a TimeoutClock and wait while block is true. See {#wait_while}
31
- def wait_while(timeout = nil, delay: nil, exception: nil, &)
31
+ def wait_while(timeout_arg = nil, timeout: timeout_arg, delay: nil, exception: nil, &)
32
32
  timeout(timeout).wait_while(exception:, delay:, &)
33
33
  end
34
34
  end
@@ -86,7 +86,7 @@ module ConcurrentMonitor
86
86
  return result
87
87
  end
88
88
 
89
- sleep([delay, self.remaining].min) if delay
89
+ sleep([delay, self.remaining || delay].min) if delay
90
90
  end
91
91
 
92
92
  raise exception, 'timed out' if exception
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConcurrentMonitor
4
+ # Shared timeout watcher that manages timeouts for multiple tasks efficiently.
5
+ # Caller is responsible for starting the watcher task.
6
+ class TimeoutWatcher
7
+ attr_reader :monitor
8
+
9
+ include ConcurrentMonitor
10
+
11
+ # @param monitor [ConcurrentMonitor::Mixin] the monitor for synchronization
12
+ def initialize(monitor:)
13
+ @monitor = monitor
14
+ @timeouts = {}
15
+ @condition = monitor.new_condition
16
+ end
17
+
18
+ # Wrap a block with timeout. Spawns the block in a new task, registers it for timeout,
19
+ # and ensures timeout is cancelled on completion.
20
+ # @param timeout [Numeric] seconds until timeout
21
+ # @yield block to execute
22
+ # @return [Task] the spawned task
23
+ def with_timeout(timeout_arg = nil, timeout: timeout_arg, **, &block)
24
+ async(**async) do |t|
25
+ watch(t, timeout: timeout) if timeout
26
+ block.call(t)
27
+ ensure
28
+ cancel_timeout(t) if timeout
29
+ end
30
+ end
31
+
32
+ # Cancel timeout for task
33
+ def cancel_timeout(task = current_task)
34
+ synchronize { @timeouts.delete(task) }
35
+ end
36
+
37
+ # Run the watcher loop. Caller should spawn this in a task.
38
+ # @example
39
+ # watcher = TimeoutWatcher.new(monitor: self)
40
+ # watcher_task = async(name: 'timeout_watcher') { watcher.run }
41
+ def run
42
+ until @stopped
43
+ synchronize do
44
+ @timeouts.delete_if { |task, clock| clock.expired?.tap { |expired| task.stop if expired } }
45
+ @condition.wait(@timeouts.values.map(&:remaining).compact.min)
46
+ end
47
+ end
48
+ end
49
+
50
+ def stop
51
+ synchronize do
52
+ @stopped = true
53
+ @condition.broadcast
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Register task to be stopped after timeout
60
+ def watch(task, timeout:)
61
+ clock = self.timeout(timeout)
62
+
63
+ synchronize do
64
+ @timeouts[task] = clock
65
+ @condition.broadcast
66
+ end
67
+ end
68
+ end
69
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # A unified Monitor interface for both Async (fibers) and Threads.
4
4
  module ConcurrentMonitor
5
- VERSION = '0.0.1'
5
+ VERSION = '0.9.0'
6
6
  end
@@ -11,7 +11,7 @@ module ConcurrentMonitor
11
11
  # @param [Exception|nil] exception an exception to raise on timeout
12
12
  # @return [Object] the truthy return value of the block
13
13
  # @return [nil] if a timeout occurs
14
- def wait_until(timeout = nil, exception: nil)
14
+ def wait_until(timeout_arg = nil, timeout: timeout_arg, exception: nil)
15
15
  TimeoutClock.wait_until(timeout, exception:) { |remaining| yield || (wait(remaining) && false) }
16
16
  end
17
17
 
@@ -6,6 +6,7 @@ require_relative 'concurrent_monitor/condition_variable'
6
6
  require_relative 'concurrent_monitor/queue'
7
7
  require_relative 'concurrent_monitor/barrier'
8
8
  require_relative 'concurrent_monitor/future'
9
+ require_relative 'concurrent_monitor/timeout_watcher'
9
10
  require 'forwardable'
10
11
 
11
12
  # A unified abstraction layer for synchronization and concurrency primitives that works
@@ -45,7 +46,7 @@ module ConcurrentMonitor
45
46
 
46
47
  # @!attribute [rw] monitor
47
48
  # @return [Async::Monitor,Thread::Monitor] should be set to an instance of Async::Monitor or Thread::Monitor
48
- def_delegators :@monitor, :sync, :async, :current_task, :synchronize, :new_condition, :task_dump
49
+ def_delegators :@monitor, :sync, :async, :current_task, :synchronize, :new_condition, :new_monitor, :task_dump
49
50
 
50
51
  # @!method new_monitor
51
52
  # @return [Async::Monitor|Thread::Monitor] a new monitor of the same kind as the current one
@@ -83,9 +84,9 @@ module ConcurrentMonitor
83
84
  # @param [Numeric|TimeoutClock] timeout
84
85
  # @param [Class|StandardError|nil] exception if set the error to raise on timeout
85
86
  # @return [Object] task result, nil if timed out without exception
86
- def with_timeout(timeout, exception: nil, condition: new_condition, &block)
87
+ def with_timeout(timeout_arg, timeout: timeout_arg, exception: nil, condition: new_condition, **kw_async, &block)
87
88
  done = false
88
- task = async do |t|
89
+ task = async(**kw_async) do |t|
89
90
  block.call(t)
90
91
  ensure
91
92
  synchronize do
@@ -95,7 +96,7 @@ module ConcurrentMonitor
95
96
  end
96
97
 
97
98
  begin
98
- synchronize { condition.wait_until(timeout, exception:) { done } }
99
+ synchronize { condition.wait_until(timeout:, exception:) { done } }
99
100
  ensure
100
101
  task.stop
101
102
  end
@@ -123,6 +124,16 @@ module ConcurrentMonitor
123
124
  Future.new(monitor:)
124
125
  end
125
126
 
127
+ def new_semaphore(limit:, monitor: self)
128
+ Semaphore.new(monitor:, limit:)
129
+ end
130
+
131
+ # Creates a new timeout watcher
132
+ # @return [TimeoutWatcher]
133
+ def new_timeout_watcher(monitor: self)
134
+ TimeoutWatcher.new(monitor:)
135
+ end
136
+
126
137
  # @!attribute [rw] monitor
127
138
  # @return [Thread::Monitor|Async::Monitor]
128
139
  attr_accessor :monitor
@@ -10,7 +10,7 @@ class Thread
10
10
  class Monitor
11
11
  # rubocop:disable Lint/InheritException
12
12
 
13
- # Raised from when a task is stopped
13
+ # Raised from stop when a task is stopped
14
14
  class Stop < Exception; end
15
15
 
16
16
  # rubocop:enable Lint/InheritException
@@ -23,17 +23,24 @@ class Thread
23
23
  # @!visibility private
24
24
  # Common task interface over Thread
25
25
  class Task < ConcurrentMonitor::Task
26
- def initialize(name = nil, report_on_exception: true, &block)
26
+ def initialize(name_arg = nil, name: name_arg, report_on_exception: true, &block)
27
27
  super()
28
28
  @stopped = nil
29
- @thread = Thread.new(self, name, report_on_exception, block) do |t, n, e, b|
30
- run_thread(t, n, e, &b)
31
- rescue Stop
32
- @stopped = true
33
- nil
34
- ensure
35
- @stopped ||= false
36
- end
29
+
30
+ @thread =
31
+ if block_given?
32
+ Thread.new(self, name, report_on_exception, block) do |t, n, e, b|
33
+ run_thread(t, n, e, &b)
34
+ rescue Stop
35
+ @stopped = true
36
+ nil
37
+ ensure
38
+ @stopped ||= false
39
+ end
40
+ else
41
+ # this is the main thread
42
+ Thread.current.tap { |t| t.thread_variable_set(:concurrent_monitor_task, self) }
43
+ end
37
44
  end
38
45
 
39
46
  def alive? = @thread.alive?
@@ -41,12 +48,13 @@ class Thread
41
48
  def current? = @thread == Thread.current
42
49
 
43
50
  def value
44
- return nil if @stopped
51
+ result = @thread.value
52
+ raise ConcurrentMonitor::TaskStopped, 'Task was stopped' if @stopped
45
53
 
46
- @thread.value
54
+ result
47
55
  rescue Stop
48
56
  @stopped = true
49
- nil
57
+ raise ConcurrentMonitor::TaskStopped, 'Task was stopped'
50
58
  end
51
59
 
52
60
  def stop
@@ -58,7 +66,7 @@ class Thread
58
66
  self # race with alive?
59
67
  end
60
68
 
61
- # must call join or value before @stopped will be true
69
+ # must call wait before @stopped will be true
62
70
  def stopped?
63
71
  !!@stopped
64
72
  end
@@ -99,7 +107,7 @@ class Thread
99
107
  end
100
108
 
101
109
  def current_task
102
- Thread.current.thread_variable_get(:concurrent_monitor_task)
110
+ Thread.current.thread_variable_get(:concurrent_monitor_task) || Task.new
103
111
  end
104
112
 
105
113
  def task_dump(io = $stderr)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concurrent_monitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.ci.release
4
+ version: 0.9.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grant Gardner
@@ -25,6 +25,7 @@ files:
25
25
  - lib/concurrent_monitor/semaphore.rb
26
26
  - lib/concurrent_monitor/task.rb
27
27
  - lib/concurrent_monitor/timeout_clock.rb
28
+ - lib/concurrent_monitor/timeout_watcher.rb
28
29
  - lib/concurrent_monitor/version.rb
29
30
  - lib/concurrent_monitor/wait_timeout.rb
30
31
  - lib/thread/monitor.rb