ztimer 0.4.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +6 -1
- data/Gemfile +4 -0
- data/README.md +42 -10
- data/lib/ztimer/slot.rb +9 -9
- data/lib/ztimer/sorted_store.rb +36 -39
- data/lib/ztimer/version.rb +4 -2
- data/lib/ztimer/watcher.rb +17 -16
- data/lib/ztimer.rb +181 -67
- data/ztimer.gemspec +17 -14
- metadata +20 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 328b728b6ba74021474c30ef9093a1bca987a368b2bd211d5545abc888447ef2
|
4
|
+
data.tar.gz: df0ff61ae1bbf3901a1ddc9fa0ac3416dce79eeb1c4e05f1d9a3f61abce722c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d482e9792d40f7083258a66cac5b1857624d02dbc6f4297a3ea608c48f8f50908dc3fe429e0666b54bd67b3b3e9b846f7869aa8ea253e980167bd2502cfd4626
|
7
|
+
data.tar.gz: d5b3ce5d4189739c8a32851383775afe84bd238d324dc26806e122f2b19fd9a4dd65509c9b04ded9aae399b3eeff19bb3a01377155408b680ed533261401b1a3
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# Ztimer
|
2
2
|
|
3
|
-
**Ztimer** is a Ruby
|
4
|
-
|
3
|
+
**Ztimer** is a simple Ruby implementation of an asynchronous timer, that allows to enqueue the execution of Ruby
|
4
|
+
code, so that it will be asynchronously executed on timeout. It's very useful when you need a simple way to execute
|
5
|
+
some code asynchronously or with a certain delay.
|
6
|
+
|
5
7
|
|
6
8
|
## Installation
|
7
9
|
|
@@ -32,6 +34,12 @@ Ztimer.after(delay) do
|
|
32
34
|
puts "Doing something useful..."
|
33
35
|
end
|
34
36
|
|
37
|
+
# Async execution
|
38
|
+
Ztimer.async do
|
39
|
+
# this code will be executed in background asyncronously
|
40
|
+
puts "Doing something useful in background..."
|
41
|
+
end
|
42
|
+
|
35
43
|
# Recurrent jobs
|
36
44
|
job = Ztimer.every(delay) do # execute the block every second
|
37
45
|
puts "Executing a recurrent job..."
|
@@ -40,17 +48,41 @@ end
|
|
40
48
|
sleep 10 # wait for 10 seconds (10 executions)
|
41
49
|
job.cancel! # cancel the recurrent job
|
42
50
|
|
51
|
+
# Custom Ztimer instance
|
52
|
+
my_timer = Ztimer.new(concurrency: 5) # create a new Ztimer instance
|
53
|
+
10.times do
|
54
|
+
# Use the custom ztimer to execute jobs asynchronously
|
55
|
+
my_timer.async do
|
56
|
+
puts "Doing async job..."
|
57
|
+
end
|
58
|
+
end
|
43
59
|
```
|
44
60
|
|
45
|
-
|
46
|
-
|
47
|
-
|
61
|
+
| Method | Description |
|
62
|
+
|--------|-------------|
|
63
|
+
| `async(&block)` | Execute the block asynchronously. |
|
64
|
+
| `after(milliseconds, &block)` | Execute the block after the specified amount of milliseconds. |
|
65
|
+
| `at(datetime, &block)` | Execute the block at the specified timestamp. |
|
66
|
+
| `every(milliseconds, start_at: nil, &block)` | Execute the block at the specified interval of milliseconds. A custom `:start_at` param could be provided to specify an offset timestamp. |
|
67
|
+
| `secondly(seconds, offset: 0, &block)` | Executes the block every N seconds. An `:offset` of seconds could be specified to shift the begining of the time slot. By default the block will be exected at the begining of the time slot. Example: `secondly(5)` will run at second `0`, `5`, `10`, `15`, etc. |
|
68
|
+
| `minutely(minutes, offset: 0, &block)` | Executes the block every N minutes. An `:offset` of minutes could be specified to shift the begining of the time slot. By default the block will be exected at the begining of the time slot. Example: `minutely(5)` will run at minute `0`, `5`, `10`, `15`, etc. |
|
69
|
+
| `hourly(hours, offset: 0, &block)` | Executes the block every N hours. An `:offset` of hours could be specified to shift the begining of the time slot. By default the block will be exected at the begining of the time slot. Example: `hourly(5)` will run at hour `0`, `5`, `10`, `15`, etc. |
|
70
|
+
| `daily(days, offset: 0, &block)` | Executes the block every N days. An `:offset` of days could be specified to shift the begining of the time slot. By default the block will be exected at the begining of the time slot. Example: `daily(5)` will run on day `0`, `5`, `10`, `15`, etc. |
|
71
|
+
| `day_of_week(day, &block)` | Execute the block only on the specified day of week. Valid days are: `"sun", "mon", "tue", "thu", "wen", "fri", "sat"`. |
|
72
|
+
| `days_of_week(days, &block)` | Execute the block on the specified days of week. |
|
48
73
|
|
49
|
-
Anyway, you can change the concurrency by calling `Ztimer.concurrency = <concurrency>`, where `<concurrency>` is the maximum number
|
50
|
-
of `Ztimer` workers allowed to run in parallel (ex: `Ztimer.concurrency = 50`).
|
51
74
|
|
52
|
-
|
75
|
+
By default **Ztimer** will run at maximum 20 jobs concurrently, so that if you have 100 jobs to be
|
76
|
+
executed at the same time, at most 20 of them will run concurrently. This is necessary in order to prevent uncontrolled threads spawn when many jobs have to be run at the same time.
|
77
|
+
|
78
|
+
Anyway, you can change the concurrency level by calling `Ztimer.concurrency = <concurrency>`, where `<concurrency>` is the maximum number of `Ztimer` workers allowed to run in parallel (ex: `Ztimer.concurrency = 50`).
|
53
79
|
|
54
|
-
|
55
|
-
|
80
|
+
If you're using custom **Ztimer** instance, you can specify the concurrency while creating the new instance:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
my_timer = Ztimer.new(concurrency: 42) # create a ztimer with concurrency set to 42
|
84
|
+
```
|
85
|
+
|
86
|
+
## Contributing
|
56
87
|
|
88
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/serioja90/ztimer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
data/lib/ztimer/slot.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
|
-
|
3
|
+
class Ztimer
|
4
|
+
# Implements a slot, which represents a block of code to be executed at specified time slot.
|
3
5
|
class Slot
|
4
6
|
attr_reader :enqueued_at, :expires_at, :recurrency, :callback
|
5
7
|
attr_accessor :started_at, :executed_at
|
6
8
|
|
7
|
-
def initialize(enqueued_at, expires_at,recurrency = -1, &callback)
|
9
|
+
def initialize(enqueued_at, expires_at, recurrency = -1, &callback)
|
8
10
|
@enqueued_at = enqueued_at
|
9
11
|
@expires_at = expires_at
|
10
12
|
@recurrency = recurrency
|
@@ -15,17 +17,15 @@ module Ztimer
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def recurrent?
|
18
|
-
|
20
|
+
@recurrency.positive?
|
19
21
|
end
|
20
22
|
|
21
23
|
def reset!
|
22
|
-
if recurrent?
|
23
|
-
@expires_at += recurrency
|
24
|
-
end
|
24
|
+
@expires_at += recurrency if recurrent?
|
25
25
|
end
|
26
26
|
|
27
27
|
def canceled?
|
28
|
-
|
28
|
+
@canceled
|
29
29
|
end
|
30
30
|
|
31
31
|
def cancel!
|
@@ -33,7 +33,7 @@ module Ztimer
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def <=>(other)
|
36
|
-
|
36
|
+
@expires_at <=> other.expires_at
|
37
37
|
end
|
38
38
|
end
|
39
|
-
end
|
39
|
+
end
|
data/lib/ztimer/sorted_store.rb
CHANGED
@@ -1,97 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
|
-
|
3
|
+
class Ztimer
|
4
|
+
# Implements a performant sorted store for time slots, which uses binary search to optimize
|
5
|
+
# new items insertion and items retrievement.
|
3
6
|
class SortedStore
|
4
|
-
|
5
7
|
def initialize
|
6
8
|
@store = []
|
7
9
|
end
|
8
10
|
|
9
11
|
def <<(value)
|
10
12
|
@store.insert(position_for(value), value)
|
11
|
-
|
13
|
+
|
14
|
+
self
|
12
15
|
end
|
13
16
|
|
14
17
|
def delete(value)
|
15
18
|
index = index_of(value)
|
16
|
-
|
17
|
-
|
18
|
-
else
|
19
|
-
return nil
|
20
|
-
end
|
19
|
+
|
20
|
+
index.nil? ? nil : @store.delete_at(index)
|
21
21
|
end
|
22
22
|
|
23
23
|
def [](index)
|
24
|
-
|
24
|
+
@store[index]
|
25
25
|
end
|
26
26
|
|
27
27
|
def first
|
28
|
-
|
28
|
+
@store.first
|
29
29
|
end
|
30
30
|
|
31
31
|
def last
|
32
|
-
|
32
|
+
@store.last
|
33
33
|
end
|
34
34
|
|
35
35
|
def shift
|
36
|
-
|
36
|
+
@store.shift
|
37
37
|
end
|
38
38
|
|
39
39
|
def pop
|
40
|
-
|
40
|
+
@store.pop
|
41
41
|
end
|
42
42
|
|
43
43
|
def index_of(value, start = 0, stop = [@store.count - 1, 0].max)
|
44
|
-
if start > stop
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
when 1 then return index_of(value, position + 1, stop)
|
54
|
-
end
|
44
|
+
return nil if start > stop
|
45
|
+
return value == @store[start] ? start : nil if start == stop
|
46
|
+
|
47
|
+
position = ((stop + start) / 2).to_i
|
48
|
+
|
49
|
+
case value <=> @store[position]
|
50
|
+
when -1 then index_of(value, start, position)
|
51
|
+
when 0 then position
|
52
|
+
when 1 then index_of(value, position + 1, stop)
|
55
53
|
end
|
56
54
|
end
|
57
55
|
|
58
56
|
def count
|
59
|
-
|
57
|
+
@store.count
|
60
58
|
end
|
61
59
|
|
62
60
|
def size
|
63
|
-
|
61
|
+
@store.size
|
64
62
|
end
|
65
63
|
|
66
64
|
def empty?
|
67
|
-
|
65
|
+
@store.empty?
|
68
66
|
end
|
69
67
|
|
70
68
|
def clear
|
71
|
-
|
69
|
+
@store.clear
|
72
70
|
end
|
73
71
|
|
74
72
|
def to_a
|
75
|
-
|
73
|
+
@store.dup
|
76
74
|
end
|
77
75
|
|
78
|
-
|
79
76
|
protected
|
80
77
|
|
81
78
|
def position_for(item, start = 0, stop = [@store.count - 1, 0].max)
|
82
|
-
if start > stop
|
83
|
-
|
84
|
-
|
79
|
+
raise "Invalid range (#{start}, #{stop})" if start > stop
|
80
|
+
|
81
|
+
if start == stop
|
85
82
|
element = @store[start]
|
86
|
-
|
83
|
+
element.nil? || ((item <=> element) <= 0) ? start : start + 1
|
87
84
|
else
|
88
|
-
position = ((stop + start)/ 2).to_i
|
85
|
+
position = ((stop + start) / 2).to_i
|
89
86
|
case item <=> @store[position]
|
90
|
-
when -1 then
|
91
|
-
when 0 then
|
92
|
-
when 1 then
|
87
|
+
when -1 then position_for(item, start, position)
|
88
|
+
when 0 then position
|
89
|
+
when 1 then position_for(item, position + 1, stop)
|
93
90
|
end
|
94
91
|
end
|
95
92
|
end
|
96
93
|
end
|
97
|
-
end
|
94
|
+
end
|
data/lib/ztimer/version.rb
CHANGED
data/lib/ztimer/watcher.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
|
-
|
3
|
+
class Ztimer
|
4
|
+
# Implements a watcher which allows to enqueue Ztimer::Slot items, that will be executed
|
5
|
+
# as soon as the time of Ztimer::Slot is reached.
|
3
6
|
class Watcher
|
4
|
-
|
5
7
|
def initialize(&callback)
|
6
8
|
@thread = nil
|
7
9
|
@slots = Ztimer::SortedStore.new
|
@@ -10,17 +12,15 @@ module Ztimer
|
|
10
12
|
@mutex = Mutex.new
|
11
13
|
end
|
12
14
|
|
13
|
-
def <<
|
15
|
+
def <<(slot)
|
14
16
|
@mutex.synchronize do
|
15
17
|
@slots << slot
|
16
|
-
if @slots.first == slot
|
17
|
-
run
|
18
|
-
end
|
18
|
+
run if @slots.first == slot
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
def jobs
|
23
|
-
|
23
|
+
@slots.size
|
24
24
|
end
|
25
25
|
|
26
26
|
protected
|
@@ -37,10 +37,11 @@ module Ztimer
|
|
37
37
|
def start
|
38
38
|
@lock.synchronize do
|
39
39
|
return if @thread
|
40
|
+
|
40
41
|
@thread = Thread.new do
|
41
42
|
loop do
|
42
43
|
begin
|
43
|
-
delay =
|
44
|
+
delay = calculate_delay
|
44
45
|
if delay.nil?
|
45
46
|
Thread.stop
|
46
47
|
next
|
@@ -48,10 +49,10 @@ module Ztimer
|
|
48
49
|
|
49
50
|
select(nil, nil, nil, delay / 1_000_000.to_f) if delay > 1 # 1 microsecond of cranularity
|
50
51
|
|
51
|
-
while
|
52
|
+
while fetch_first_expired
|
52
53
|
end
|
53
|
-
rescue => e
|
54
|
-
puts e.inspect
|
54
|
+
rescue StandardError => e
|
55
|
+
puts "#{e.inspect}\n#{e.backtrace.join("\n")}"
|
55
56
|
end
|
56
57
|
end
|
57
58
|
end
|
@@ -59,11 +60,11 @@ module Ztimer
|
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
62
|
-
def
|
63
|
-
|
63
|
+
def calculate_delay
|
64
|
+
@mutex.synchronize { @slots.empty? ? nil : @slots.first.expires_at - utc_microseconds }
|
64
65
|
end
|
65
66
|
|
66
|
-
def
|
67
|
+
def fetch_first_expired
|
67
68
|
@mutex.synchronize do
|
68
69
|
slot = @slots.first
|
69
70
|
if slot && (slot.expires_at < utc_microseconds)
|
@@ -89,7 +90,7 @@ module Ztimer
|
|
89
90
|
end
|
90
91
|
|
91
92
|
def utc_microseconds
|
92
|
-
|
93
|
+
Time.now.to_f * 1_000_000
|
93
94
|
end
|
94
95
|
end
|
95
|
-
end
|
96
|
+
end
|
data/lib/ztimer.rb
CHANGED
@@ -1,83 +1,197 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
return slot
|
25
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ztimer/version'
|
4
|
+
require 'ztimer/slot'
|
5
|
+
require 'ztimer/sorted_store'
|
6
|
+
require 'ztimer/watcher'
|
7
|
+
|
8
|
+
# Implements a timer which allows to execute a block with a delay, recurrently or asynchronously.
|
9
|
+
class Ztimer
|
10
|
+
@default_instance = nil
|
11
|
+
|
12
|
+
attr_reader :concurrency, :running, :count, :watcher, :queue
|
13
|
+
|
14
|
+
def initialize(concurrency: 20)
|
15
|
+
@concurrency = concurrency
|
16
|
+
@watcher = Ztimer::Watcher.new { |slot| execute(slot) }
|
17
|
+
@workers_lock = Mutex.new
|
18
|
+
@count_lock = Mutex.new
|
19
|
+
@queue = Queue.new
|
20
|
+
@running = 0
|
21
|
+
@count = 0
|
22
|
+
end
|
26
23
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
# Execute the code block asyncrhonously right now
|
25
|
+
def async(&callback)
|
26
|
+
enqueued_at = utc_microseconds
|
27
|
+
slot = Slot.new(enqueued_at, enqueued_at, -1, &callback)
|
31
28
|
|
32
|
-
|
29
|
+
incr_counter!
|
30
|
+
execute(slot)
|
33
31
|
|
34
|
-
|
35
|
-
|
32
|
+
slot
|
33
|
+
end
|
36
34
|
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
# Execute the code block after the specified delay
|
36
|
+
def after(milliseconds, &callback)
|
37
|
+
enqueued_at = utc_microseconds
|
38
|
+
expires_at = enqueued_at + milliseconds * 1000
|
39
|
+
slot = Slot.new(enqueued_at, expires_at, -1, &callback)
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
add(slot)
|
42
|
+
|
43
|
+
slot
|
44
|
+
end
|
45
|
+
|
46
|
+
# Execute the code block at a specific datetime
|
47
|
+
def at(datetime, &callback)
|
48
|
+
enqueued_at = datetime.to_f * 1_000_000
|
49
|
+
|
50
|
+
slot = Slot.new(enqueued_at, enqueued_at, -1, &callback)
|
51
|
+
add(slot)
|
52
|
+
|
53
|
+
slot
|
54
|
+
end
|
55
|
+
|
56
|
+
# Execute the code block every N milliseconds.
|
57
|
+
# When :start_at is specified, the first execution will start at specified date/time
|
58
|
+
def every(milliseconds, start_at: nil, &callback)
|
59
|
+
enqueued_at = start_at ? start_at.to_f * 1_000_000 : utc_microseconds
|
60
|
+
expires_at = enqueued_at + milliseconds * 1000
|
61
|
+
slot = Slot.new(enqueued_at, expires_at, milliseconds * 1000, &callback)
|
45
62
|
|
46
|
-
|
63
|
+
add(slot)
|
64
|
+
|
65
|
+
slot
|
66
|
+
end
|
67
|
+
|
68
|
+
# Run ztimer every N seconds, starting with the nearest time slot (ex. secondly(5)
|
69
|
+
# will run at second 0, 5, 10, 15, etc.)
|
70
|
+
def secondly(seconds, offset: 0, &callback)
|
71
|
+
start_time = utc_microseconds
|
72
|
+
milliseconds = (seconds.to_f * 1000).to_i
|
73
|
+
enqueued_at = start_time - (start_time % (milliseconds * 1000)) + offset * 1_000_000
|
74
|
+
expires_at = enqueued_at + milliseconds * 1000
|
75
|
+
|
76
|
+
slot = Slot.new(enqueued_at, expires_at, milliseconds * 1000, &callback)
|
77
|
+
add(slot)
|
78
|
+
|
79
|
+
slot
|
80
|
+
end
|
81
|
+
|
82
|
+
# Run ztimer every N minutes, starting at the nearest time slot (ex. minutely(2) will run at minute 0, 2, 4, 6, etc.)
|
83
|
+
def minutely(minutes, offset: 0, &callback)
|
84
|
+
secondly(minutes.to_f * 60, offset: offset.to_f * 60, &callback)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Run ztimer every N hours, starting at the nearest time slot (ex. hourly(2) will run at hour 0, 2, 4, 6, etc.)
|
88
|
+
def hourly(hours, offset: 0, &callback)
|
89
|
+
minutely(hours.to_f * 60, offset: offset.to_f * 60, &callback)
|
90
|
+
end
|
91
|
+
|
92
|
+
def daily(days, offset: 0, &callback)
|
93
|
+
raise ArgumentError, "Days number should be > 0: #{days.inspect}" if days.to_f <= 0
|
94
|
+
|
95
|
+
hourly(days.to_f * 24, offset: offset.to_f * 24, &callback)
|
96
|
+
end
|
47
97
|
|
48
|
-
|
49
|
-
|
50
|
-
|
98
|
+
def day_of_week(day, &callback)
|
99
|
+
days = %w[sun mon tue thu wen fri sat]
|
100
|
+
current_day = Time.now.wday
|
101
|
+
|
102
|
+
index = day.to_i
|
103
|
+
if day.is_a?(String)
|
104
|
+
# Find day number by day name
|
105
|
+
index = days.index { |day_name| day.strip.downcase == day_name }
|
106
|
+
raise ArgumentError, "Invalid week day: #{day.inspect}" if index.nil?
|
107
|
+
elsif index.negative? || index > 6
|
108
|
+
raise ArgumentError, "Invalid week day: #{day.inspect}"
|
51
109
|
end
|
52
110
|
|
111
|
+
offset = 0
|
112
|
+
offset = (current_day > index ? index - current_day : current_day - index) if current_day != index
|
53
113
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
114
|
+
daily(7, offset: offset, &callback)
|
115
|
+
end
|
116
|
+
|
117
|
+
def days_of_week(*args, &callback)
|
118
|
+
args.map { |day| day_of_week(day, &callback) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def jobs_count
|
122
|
+
@watcher.jobs
|
123
|
+
end
|
124
|
+
|
125
|
+
def concurrency=(new_value)
|
126
|
+
value_is_integer = new_value.is_a?(Integer)
|
127
|
+
raise ArgumentError, "Invalid concurrency value: #{new_value}" unless value_is_integer && new_value >= 1
|
128
|
+
|
129
|
+
@concurrency = new_value
|
130
|
+
end
|
131
|
+
|
132
|
+
def stats
|
133
|
+
{
|
134
|
+
running: @running,
|
135
|
+
scheduled: @watcher.jobs,
|
136
|
+
executing: @queue.size,
|
137
|
+
total: @count
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.method_missing(name, *args, &block)
|
142
|
+
@default_instance ||= Ztimer.new(concurrency: 20)
|
143
|
+
@default_instance.send(name, *args, &block)
|
144
|
+
end
|
145
|
+
|
146
|
+
protected
|
147
|
+
|
148
|
+
def add(slot)
|
149
|
+
incr_counter!
|
150
|
+
@watcher << slot
|
151
|
+
end
|
152
|
+
|
153
|
+
def incr_counter!
|
154
|
+
@count_lock.synchronize{ @count += 1 }
|
155
|
+
end
|
156
|
+
|
157
|
+
def execute(slot)
|
158
|
+
@queue << slot
|
159
|
+
|
160
|
+
@workers_lock.synchronize do
|
161
|
+
[@concurrency - @running, @queue.size].min.times do
|
162
|
+
@running += 1
|
163
|
+
start_new_thread!
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def start_new_thread!
|
169
|
+
worker = Thread.new do
|
170
|
+
begin
|
171
|
+
loop do
|
172
|
+
current_slot = nil
|
173
|
+
@workers_lock.synchronize do
|
174
|
+
current_slot = @queue.pop(true) unless @queue.empty?
|
175
|
+
end
|
176
|
+
break if current_slot.nil?
|
177
|
+
|
178
|
+
begin
|
179
|
+
current_slot.executed_at = utc_microseconds
|
180
|
+
current_slot.callback.call(current_slot) unless current_slot.callback.nil? || current_slot.canceled?
|
181
|
+
rescue StandardError => e
|
182
|
+
backtrace = e.backtrace ? "\n#{e.backtrace.join("\n")}" : ''
|
183
|
+
warn e.inspect + backtrace
|
73
184
|
end
|
74
|
-
worker.abort_on_exception = true
|
75
185
|
end
|
186
|
+
rescue ThreadError
|
187
|
+
puts 'queue is empty'
|
76
188
|
end
|
189
|
+
@workers_lock.synchronize { @running -= 1 }
|
77
190
|
end
|
191
|
+
worker.abort_on_exception = true
|
192
|
+
end
|
78
193
|
|
79
|
-
|
80
|
-
|
81
|
-
end
|
194
|
+
def utc_microseconds
|
195
|
+
Time.now.to_f * 1_000_000
|
82
196
|
end
|
83
|
-
end
|
197
|
+
end
|
data/ztimer.gemspec
CHANGED
@@ -1,25 +1,28 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'ztimer/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
+
spec.name = 'ztimer'
|
8
9
|
spec.version = Ztimer::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
10
|
+
spec.authors = ['Groza Sergiu']
|
11
|
+
spec.email = ['serioja90@gmail.com']
|
11
12
|
|
12
|
-
spec.summary = %
|
13
|
-
spec.description = %
|
14
|
-
spec.homepage =
|
15
|
-
spec.license =
|
13
|
+
spec.summary = %(An asyncrhonous timer)
|
14
|
+
spec.description = %(Ruby asyncrhonous timer that allows you to enqueue tasks to be executed asyncrhonously after a delay)
|
15
|
+
spec.homepage = 'https://github.com/serioja90/ztimer'
|
16
|
+
spec.license = 'MIT'
|
16
17
|
|
17
18
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
-
spec.bindir =
|
19
|
+
spec.bindir = 'exe'
|
19
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
-
spec.require_paths = [
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.required_ruby_version = '>= 2.5'
|
21
24
|
|
22
|
-
spec.add_development_dependency
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
25
|
+
spec.add_development_dependency 'bundler', '~>2.2', '>= 2.2.33'
|
26
|
+
spec.add_development_dependency 'rake', '~>12.3', '>= 12.3.3'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
25
28
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ztimer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Groza Sergiu
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,28 +16,40 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.2'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.2.33
|
20
23
|
type: :development
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - "~>"
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
29
|
+
version: '2.2'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.2.33
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rake
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
37
|
- - "~>"
|
32
38
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
39
|
+
version: '12.3'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 12.3.3
|
34
43
|
type: :development
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
37
46
|
requirements:
|
38
47
|
- - "~>"
|
39
48
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
49
|
+
version: '12.3'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 12.3.3
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: rspec
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,17 +99,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
99
|
requirements:
|
88
100
|
- - ">="
|
89
101
|
- !ruby/object:Gem::Version
|
90
|
-
version: '
|
102
|
+
version: '2.5'
|
91
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
104
|
requirements:
|
93
105
|
- - ">="
|
94
106
|
- !ruby/object:Gem::Version
|
95
107
|
version: '0'
|
96
108
|
requirements: []
|
97
|
-
|
98
|
-
rubygems_version: 2.4.6
|
109
|
+
rubygems_version: 3.0.8
|
99
110
|
signing_key:
|
100
111
|
specification_version: 4
|
101
112
|
summary: An asyncrhonous timer
|
102
113
|
test_files: []
|
103
|
-
has_rdoc:
|