ztimer
Advanced tools
+6
-1
| language: ruby | ||
| rvm: | ||
| - 2.2.1 | ||
| - 2.2 | ||
| before_install: gem install bundler -v 1.11.2 | ||
| script: | ||
| - gem build ztimer.gemspec | ||
| - gem install ztimer --local | ||
| - bundle exec rspec |
+4
-0
@@ -5,1 +5,5 @@ source 'https://rubygems.org' | ||
| gemspec | ||
| group :test do | ||
| gem 'lounger', '~> 0.2.0' | ||
| end |
+92
-84
@@ -6,114 +6,122 @@ require "ztimer/version" | ||
| module Ztimer | ||
| @concurrency = 20 | ||
| @watcher = Ztimer::Watcher.new(){|slot| execute(slot) } | ||
| @workers_lock = Mutex.new | ||
| @count_lock = Mutex.new | ||
| @queue = Queue.new | ||
| @running = 0 | ||
| @count = 0 | ||
| class Ztimer | ||
| @default_instance = nil | ||
| class << self | ||
| attr_reader :concurrency, :running, :count, :watcher, :queue | ||
| attr_reader :concurrency, :running, :count, :watcher, :queue | ||
| def async(&callback) | ||
| enqueued_at = utc_microseconds | ||
| slot = Slot.new(enqueued_at, enqueued_at, -1, &callback) | ||
| def initialize(concurrency: 20) | ||
| @concurrency = concurrency | ||
| @watcher = Ztimer::Watcher.new(){|slot| execute(slot) } | ||
| @workers_lock = Mutex.new | ||
| @count_lock = Mutex.new | ||
| @queue = Queue.new | ||
| @running = 0 | ||
| @count = 0 | ||
| end | ||
| incr_counter! | ||
| execute(slot) | ||
| def async(&callback) | ||
| enqueued_at = utc_microseconds | ||
| slot = Slot.new(enqueued_at, enqueued_at, -1, &callback) | ||
| return slot | ||
| end | ||
| incr_counter! | ||
| execute(slot) | ||
| def after(milliseconds, &callback) | ||
| enqueued_at = utc_microseconds | ||
| expires_at = enqueued_at + milliseconds * 1000 | ||
| slot = Slot.new(enqueued_at, expires_at, -1, &callback) | ||
| return slot | ||
| end | ||
| add(slot) | ||
| def after(milliseconds, &callback) | ||
| enqueued_at = utc_microseconds | ||
| expires_at = enqueued_at + milliseconds * 1000 | ||
| slot = Slot.new(enqueued_at, expires_at, -1, &callback) | ||
| return slot | ||
| end | ||
| add(slot) | ||
| def every(milliseconds, &callback) | ||
| enqueued_at = utc_microseconds | ||
| expires_at = enqueued_at + milliseconds * 1000 | ||
| slot = Slot.new(enqueued_at, expires_at, milliseconds * 1000, &callback) | ||
| return slot | ||
| end | ||
| add(slot) | ||
| def every(milliseconds, &callback) | ||
| enqueued_at = utc_microseconds | ||
| expires_at = enqueued_at + milliseconds * 1000 | ||
| slot = Slot.new(enqueued_at, expires_at, milliseconds * 1000, &callback) | ||
| return slot | ||
| end | ||
| add(slot) | ||
| def jobs_count | ||
| return @watcher.jobs | ||
| end | ||
| return slot | ||
| end | ||
| def concurrency=(new_value) | ||
| raise ArgumentError.new("Invalid concurrency value: #{new_value}") unless new_value.is_a?(Fixnum) && new_value >= 1 | ||
| @concurrency = new_value | ||
| end | ||
| def jobs_count | ||
| return @watcher.jobs | ||
| end | ||
| def concurrency=(new_value) | ||
| raise ArgumentError.new("Invalid concurrency value: #{new_value}") unless new_value.is_a?(Fixnum) && new_value >= 1 | ||
| @concurrency = new_value | ||
| end | ||
| def stats | ||
| { | ||
| running: @running, | ||
| scheduled: @watcher.jobs, | ||
| executing: @queue.size, | ||
| total: @count | ||
| } | ||
| end | ||
| protected | ||
| def stats | ||
| { | ||
| running: @running, | ||
| scheduled: @watcher.jobs, | ||
| executing: @queue.size, | ||
| total: @count | ||
| } | ||
| end | ||
| def add(slot) | ||
| incr_counter! | ||
| @watcher << slot | ||
| end | ||
| def incr_counter! | ||
| @count_lock.synchronize{ @count += 1 } | ||
| end | ||
| def self.method_missing(name, *args, &block) | ||
| @default_instance ||= Ztimer.new(concurrency: 20) | ||
| @default_instance.send(name, *args, &block) | ||
| end | ||
| def execute(slot) | ||
| @queue << slot | ||
| protected | ||
| @workers_lock.synchronize do | ||
| [@concurrency - @running, @queue.size].min.times do | ||
| @running += 1 | ||
| start_new_thread! | ||
| end | ||
| def add(slot) | ||
| incr_counter! | ||
| @watcher << slot | ||
| end | ||
| def incr_counter! | ||
| @count_lock.synchronize{ @count += 1 } | ||
| end | ||
| def execute(slot) | ||
| @queue << slot | ||
| @workers_lock.synchronize do | ||
| [@concurrency - @running, @queue.size].min.times do | ||
| @running += 1 | ||
| start_new_thread! | ||
| end | ||
| end | ||
| end | ||
| def start_new_thread! | ||
| worker = Thread.new do | ||
| begin | ||
| loop do | ||
| current_slot = nil | ||
| @workers_lock.synchronize do | ||
| current_slot = @queue.pop(true) unless @queue.empty? | ||
| end | ||
| break if current_slot.nil? | ||
| def start_new_thread! | ||
| worker = Thread.new do | ||
| begin | ||
| loop do | ||
| current_slot = nil | ||
| @workers_lock.synchronize do | ||
| current_slot = @queue.pop(true) unless @queue.empty? | ||
| end | ||
| break if current_slot.nil? | ||
| begin | ||
| current_slot.executed_at = utc_microseconds | ||
| current_slot.callback.call(current_slot) unless current_slot.callback.nil? || current_slot.canceled? | ||
| rescue => e | ||
| STDERR.puts e.inspect + (e.backtrace ? "\n" + e.backtrace.join("\n") : "") | ||
| end | ||
| begin | ||
| current_slot.executed_at = utc_microseconds | ||
| current_slot.callback.call(current_slot) unless current_slot.callback.nil? || current_slot.canceled? | ||
| rescue => e | ||
| STDERR.puts e.inspect + (e.backtrace ? "\n" + e.backtrace.join("\n") : "") | ||
| end | ||
| rescue ThreadError | ||
| puts "queue is empty" | ||
| end | ||
| @workers_lock.synchronize { @running -= 1 } | ||
| rescue ThreadError | ||
| puts "queue is empty" | ||
| end | ||
| worker.abort_on_exception = true | ||
| @workers_lock.synchronize { @running -= 1 } | ||
| end | ||
| worker.abort_on_exception = true | ||
| end | ||
| def utc_microseconds | ||
| return Time.now.to_f * 1_000_000 | ||
| end | ||
| def utc_microseconds | ||
| return Time.now.to_f * 1_000_000 | ||
| end | ||
| end | ||
| end |
| module Ztimer | ||
| class Ztimer | ||
| class Slot | ||
@@ -39,2 +39,2 @@ attr_reader :enqueued_at, :expires_at, :recurrency, :callback | ||
| end | ||
| end | ||
| end |
| module Ztimer | ||
| class Ztimer | ||
| class SortedStore | ||
@@ -97,2 +97,2 @@ | ||
| end | ||
| end | ||
| end |
@@ -1,3 +0,3 @@ | ||
| module Ztimer | ||
| VERSION = "0.5.0" | ||
| class Ztimer | ||
| VERSION = "0.6.0" | ||
| end |
| module Ztimer | ||
| class Ztimer | ||
| class Watcher | ||
@@ -95,2 +95,2 @@ | ||
| end | ||
| end | ||
| end |
+20
-5
| # Ztimer | ||
| **Ztimer** is a Ruby gem that allows to get asynchronous delayed notifications. You can enqueue callbacks to be | ||
| called after some amount of time. | ||
| **Ztimer** is a simple Ruby implementation of an asynchronous timer, that allows to enqueue the execution of Ruby | ||
| code, so that it will be asynchronously executed on timeout. It's very useful when you need a simple way to execute | ||
| some code asynchronously or with a certain delay. | ||
| ## Installation | ||
@@ -49,12 +51,25 @@ | ||
| # Custom Ztimer instance | ||
| my_timer = Ztimer.new(concurrency: 5) # create a new Ztimer instance | ||
| 10.times do | ||
| # Use the custom ztimer to execute jobs asynchronously | ||
| my_timer.async do | ||
| puts "Doing async job..." | ||
| end | ||
| end | ||
| ``` | ||
| By default **Ztimer** will run at maximum 20 jobs concurrently, so that if you have 100 jobs to be | ||
| executed at the same time, at maximum 20 of them will run at the same time. This is necessary in order to prevent uncontrolled threads spawn when many jobs have to be sent at the same time. | ||
| 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. | ||
| Anyway, you can change the concurrency by calling `Ztimer.concurrency = <concurrency>`, where `<concurrency>` is the maximum number of `Ztimer` workers allowed to run in parallel (ex: `Ztimer.concurrency = 50`). | ||
| 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`). | ||
| If you're using custom **Ztimer** instance, you can specify the concurrency while creating the new instance: | ||
| ```ruby | ||
| my_timer = Ztimer.new(concurrency: 42) # create a ztimer with concurrency set to 42 | ||
| ``` | ||
| ## Contributing | ||
| 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. | ||