Repository: socketry/timers Branch: main Commit: 23bccc713a24 Files: 35 Total size: 51.4 KB Directory structure: gitextract_wr79o4sy/ ├── .editorconfig ├── .git-blame-ignore-revs ├── .github/ │ └── workflows/ │ ├── documentation-coverage.yaml │ ├── documentation.yaml │ ├── rubocop.yaml │ ├── test-coverage.yaml │ └── test.yaml ├── .gitignore ├── .mailmap ├── .rubocop.yml ├── config/ │ └── sus.rb ├── fixtures/ │ └── timer_quantum.rb ├── gems.rb ├── lib/ │ ├── timers/ │ │ ├── events.rb │ │ ├── group.rb │ │ ├── interval.rb │ │ ├── priority_heap.rb │ │ ├── timer.rb │ │ ├── version.rb │ │ └── wait.rb │ └── timers.rb ├── license.md ├── readme.md ├── release.cert ├── test/ │ └── timers/ │ ├── events.rb │ ├── group/ │ │ ├── cancel.rb │ │ ├── every.rb │ │ └── pause.rb │ ├── group.rb │ ├── performance.rb │ ├── priority_heap.rb │ ├── strict.rb │ ├── timer.rb │ └── wait.rb └── timers.gemspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab indent_size = 2 [*.{yml,yaml}] indent_style = space indent_size = 2 ================================================ FILE: .git-blame-ignore-revs ================================================ 9180068e70c5b5c1fdb9a6c47f4d8f2553fc7104 ================================================ FILE: .github/workflows/documentation-coverage.yaml ================================================ name: Documentation Coverage on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm COVERAGE: PartialSummary jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - name: Validate coverage timeout-minutes: 5 run: bundle exec bake decode:index:coverage lib ================================================ FILE: .github/workflows/documentation.yaml ================================================ name: Documentation on: push: branches: - main # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: permissions: contents: read pages: write id-token: write # Allow one concurrent deployment: concurrency: group: "pages" cancel-in-progress: true env: CONSOLE_OUTPUT: XTerm BUNDLE_WITH: maintenance jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - name: Installing packages run: sudo apt-get install wget - name: Generate documentation timeout-minutes: 5 run: bundle exec bake utopia:project:static --force no - name: Upload documentation artifact uses: actions/upload-pages-artifact@v3 with: path: docs deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{steps.deployment.outputs.page_url}} needs: generate steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/rubocop.yaml ================================================ name: RuboCop on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Run RuboCop timeout-minutes: 10 run: bundle exec rubocop ================================================ FILE: .github/workflows/test-coverage.yaml ================================================ name: Test Coverage on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm COVERAGE: PartialSummary jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "3.4" steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 5 run: bundle exec bake test - uses: actions/upload-artifact@v4 with: include-hidden-files: true if-no-files-found: error name: coverage-${{matrix.os}}-${{matrix.ruby}} path: .covered.db validate: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - uses: actions/download-artifact@v4 - name: Validate coverage timeout-minutes: 5 run: bundle exec bake covered:validate --paths */.covered.db \; ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - "3.1" - "3.2" - "3.3" - "3.4" experimental: [false] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 10 run: bundle exec bake test ================================================ FILE: .gitignore ================================================ /.bundle/ /pkg/ /gems.locked /.covered.db /external ================================================ FILE: .mailmap ================================================ Nicholas Evans Ron Evans Sean Gregory Utenmiki Donovan Keme Donovan Keme Donovan Keme Utenmiki Tommy Ong Gia Phu Ryunosuke Sato ================================================ FILE: .rubocop.yml ================================================ AllCops: DisabledByDefault: true Layout/IndentationStyle: Enabled: true EnforcedStyle: tabs Layout/InitialIndentation: Enabled: true Layout/IndentationWidth: Enabled: true Width: 1 Layout/IndentationConsistency: Enabled: true EnforcedStyle: normal Layout/BlockAlignment: Enabled: true Layout/EndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/BeginEndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/ElseAlignment: Enabled: true Layout/DefEndAlignment: Enabled: true Layout/CaseIndentation: Enabled: true Layout/CommentIndentation: Enabled: true Layout/EmptyLinesAroundClassBody: Enabled: true Layout/EmptyLinesAroundModuleBody: Enabled: true Style/FrozenStringLiteralComment: Enabled: true Style/StringLiterals: Enabled: true EnforcedStyle: double_quotes ================================================ FILE: config/sus.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2025, by Samuel Williams. require "covered/sus" include Covered::Sus ================================================ FILE: fixtures/timer_quantum.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2025, by Samuel Williams. class TimerQuantum def self.resolve self.new.to_f end def to_f precision end private def precision @precision ||= self.measure_host_precision end def measure_host_precision(repeats: 100, duration: 0.01) # Measure the precision sleep using the monotonic clock: start_time = self.now repeats.times do sleep(duration) end end_time = self.now actual_duration = end_time - start_time expected_duration = repeats * duration if actual_duration < expected_duration warn "Invalid precision measurement: #{actual_duration} < #{expected_duration}" return 0.1 end # This computes the overhead of sleep, called `repeats` times: return actual_duration - expected_duration end def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end end TIMER_QUANTUM = TimerQuantum.resolve ================================================ FILE: gems.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2016, by Tony Arcieri. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2015, by Donovan Keme. source "https://rubygems.org" gemspec group :maintenance, optional: true do gem "bake-modernize" gem "bake-gem" end group :test do gem "sus" gem "covered" gem "decode" gem "rubocop" gem "bake-test" gem "bake-test-external" gem "benchmark-ips" gem "ruby-prof", platform: :mri end ================================================ FILE: lib/timers/events.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2022, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. # Copyright, 2014, by Lavir the Whiolet. # Copyright, 2015, by Utenmiki. # Copyright, 2015, by Donovan Keme. # Copyright, 2021, by Wander Hillen. require_relative "timer" require_relative "priority_heap" module Timers # Maintains a PriorityHeap of events ordered on time, which can be cancelled. class Events # Represents a cancellable handle for a specific timer event. class Handle include Comparable def initialize(time, callback) @time = time @callback = callback end # The absolute time that the handle should be fired at. attr_reader :time # Cancel this timer, O(1). def cancel! # The simplest way to keep track of cancelled status is to nullify the # callback. This should also be optimal for garbage collection. @callback = nil end # Has this timer been cancelled? Cancelled timer's don't fire. def cancelled? @callback.nil? end def <=> other @time <=> other.time end # Fire the callback if not cancelled with the given time parameter. def fire(time) @callback.call(time) if @callback end end def initialize # A sequence of handles, maintained in sorted order, future to present. # @sequence.last is the next event to be fired. @sequence = PriorityHeap.new @queue = [] end # Add an event at the given time. def schedule(time, callback) flush! handle = Handle.new(time.to_f, callback) @queue << handle return handle end # Returns the first non-cancelled handle. def first merge! while (handle = @sequence.peek) return handle unless handle.cancelled? @sequence.pop end end # Returns the number of pending (possibly cancelled) events. def size @sequence.size + @queue.size end # Fire all handles for which Handle#time is less than the given time. def fire(time) merge! while handle = @sequence.peek and handle.time <= time @sequence.pop handle.fire(time) end end private # Move all non-cancelled timers from the pending queue to the priority heap def merge! while handle = @queue.pop next if handle.cancelled? @sequence.push(handle) end end def flush! while @queue.last&.cancelled? @queue.pop end end end end ================================================ FILE: lib/timers/group.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. # Copyright, 2015, by Donovan Keme. # Copyright, 2015, by Tommy Ong Gia Phu. require "set" require "forwardable" require_relative "interval" require_relative "timer" require_relative "events" module Timers # A collection of timers which may fire at different times class Group include Enumerable extend Forwardable def_delegators :@timers, :each, :empty? def initialize @events = Events.new @timers = Set.new @paused_timers = Set.new @interval = Interval.new @interval.start end # Scheduled events: attr_reader :events # Active timers: attr_reader :timers # Paused timers: attr_reader :paused_timers # Call the given block after the given interval. The first argument will be # the time at which the group was asked to fire timers for. def after(interval, &block) Timer.new(self, interval, false, &block) end # Call the given block immediately, and then after the given interval. The first # argument will be the time at which the group was asked to fire timers for. def now_and_after(interval, &block) yield after(interval, &block) end # Call the given block periodically at the given interval. The first # argument will be the time at which the group was asked to fire timers for. def every(interval, recur = true, &block) Timer.new(self, interval, recur, &block) end # Call the given block immediately, and then periodically at the given interval. The first # argument will be the time at which the group was asked to fire timers for. def now_and_every(interval, recur = true, &block) yield every(interval, recur, &block) end # Wait for the next timer and fire it. Can take a block, which should behave # like sleep(n), except that n may be nil (sleep forever) or a negative # number (fire immediately after return). def wait if block_given? yield wait_interval while (interval = wait_interval) && interval > 0 yield interval end else while (interval = wait_interval) && interval > 0 # We cannot assume that sleep will wait for the specified time, it might be +/- a bit. sleep interval end end fire end # Interval to wait until when the next timer will fire. # - nil: no timers # - -ve: timers expired already # - 0: timers ready to fire # - +ve: timers waiting to fire def wait_interval(offset = current_offset) if handle = @events.first handle.time - Float(offset) end end # Fire all timers that are ready. def fire(offset = current_offset) @events.fire(offset) end # Pause all timers. def pause @timers.dup.each(&:pause) end # Resume all timers. def resume @paused_timers.dup.each(&:resume) end alias continue resume # Delay all timers. def delay(seconds) @timers.each do |timer| timer.delay(seconds) end end # Cancel all timers. def cancel @timers.dup.each(&:cancel) end # The group's current time. def current_offset @interval.to_f end end end ================================================ FILE: lib/timers/interval.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2018-2022, by Samuel Williams. module Timers # A collection of timers which may fire at different times class Interval # Get the current elapsed monotonic time. def initialize @total = 0.0 @current = nil end def start return if @current @current = now end def stop return unless @current @total += duration @current = nil end def to_f @total + duration end protected def duration now - @current end protected def now ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) end end end ================================================ FILE: lib/timers/priority_heap.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021, by Wander Hillen. # Copyright, 2021-2025, by Samuel Williams. module Timers # A priority queue implementation using a standard binary minheap. It uses straight comparison # of its contents to determine priority. This works because a Handle from Timers::Events implements # the '<' operation by comparing the expiry time. # See for explanations of the main methods. class PriorityHeap def initialize # The heap is represented with an array containing a binary tree. See # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array # is built up. @contents = [] end # Returns the earliest timer or nil if the heap is empty. def peek @contents[0] end # Returns the number of elements in the heap def size @contents.size end # Returns the earliest timer if the heap is non-empty and removes it from the heap. # Returns nil if the heap is empty. (and doesn't change the heap in that case) def pop # If the heap is empty: if @contents.empty? return nil end # If we have only one item, no swapping is required: if @contents.size == 1 return @contents.pop end # Take the root of the tree: value = @contents[0] # Remove the last item in the tree: last = @contents.pop # Overwrite the root of the tree with the item: @contents[0] = last # Bubble it down into place: bubble_down(0) # validate! return value end # Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again. def push(element) # Insert the item at the end of the heap: @contents.push(element) # Bubble it up into position: bubble_up(@contents.size - 1) # validate! return self end # Empties out the heap, discarding all elements def clear! @contents = [] end # Validate the heap invariant. Every element except the root must not be smaller than # its parent element. Note that it MAY be equal. def valid? # notice we skip index 0 on purpose, because it has no parent (1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] } end private # Left here for reference, but unused. # def swap(i, j) # @contents[i], @contents[j] = @contents[j], @contents[i] # end def bubble_up(index) parent_index = (index - 1) / 2 # watch out, integer division! while index > 0 && @contents[index] < @contents[parent_index] # if the node has a smaller value than its parent, swap these nodes # to uphold the minheap invariant and update the index of the 'current' # node. If the node is already at index 0, we can also stop because that # is the root of the heap. # swap(index, parent_index) @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index] index = parent_index parent_index = (index - 1) / 2 # watch out, integer division! end end def bubble_down(index) swap_value = 0 swap_index = nil while true left_index = (2 * index) + 1 left_value = @contents[left_index] if left_value.nil? # This node has no children so it can't bubble down any further. # We're done here! return end # Determine which of the child nodes has the smallest value: right_index = left_index + 1 right_value = @contents[right_index] if right_value.nil? or right_value > left_value swap_value = left_value swap_index = left_index else swap_value = right_value swap_index = right_index end if @contents[index] < swap_value # No need to swap, the minheap invariant is already satisfied: return else # At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further: # swap(index, swap_index) @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index] index = swap_index end end end end end ================================================ FILE: lib/timers/timer.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2017, by Tony Arcieri. # Copyright, 2014, by Utenmiki. # Copyright, 2014, by Lin Jen-Shin. # Copyright, 2017, by Vít Ondruch. # Copyright, 2025, by Patrik Wenger. module Timers # An individual timer set to fire a given proc at a given time. A timer is # always connected to a Timer::Group but it would ONLY be in @group.timers # if it also has a @handle specified. Otherwise it is either PAUSED or has # been FIRED and is not recurring. You can manually enter this state by # calling #cancel and resume normal operation by calling #reset. class Timer include Comparable attr_reader :interval, :offset, :recurring def initialize(group, interval, recurring = false, offset = nil, &block) @group = group @interval = interval @recurring = recurring @block = block @offset = nil @handle = nil # If a start offset was supplied, use that, otherwise use the current timers offset. reset(offset || @group.current_offset) end def paused? @group.paused_timers.include? self end def pause return if paused? @group.timers.delete self @group.paused_timers.add self @handle.cancel! if @handle @handle = nil end def resume return unless paused? @group.paused_timers.delete self # This will add us back to the group: reset end alias continue resume # Extend this timer def delay(seconds) @handle.cancel! if @handle @offset += seconds @handle = @group.events.schedule(@offset, self) end # Cancel this timer. Do not call while paused. def cancel return unless @handle @handle.cancel! if @handle @handle = nil # This timer is no longer valid: @group.timers.delete(self) if @group end # Reset this timer. Do not call while paused. # @param offset [Numeric] the duration to add to the timer. def reset(offset = @group.current_offset) # This logic allows us to minimise the interaction with @group.timers. # A timer with a handle is always registered with the group. if @handle @handle.cancel! else @group.timers << self end @offset = Float(offset) + @interval @handle = @group.events.schedule(@offset, self) end # Fire the block. def fire(offset = @group.current_offset) if recurring == :strict # ... make the next interval strictly the last offset + the interval: reset(@offset) elsif recurring reset(offset) else @offset = offset end result = @block.call(offset, self) cancel unless recurring result end alias call fire # Number of seconds until next fire / since last fire def fires_in @offset - @group.current_offset if @offset end # Inspect a timer def inspect buffer = to_s[0..-2] if @offset delta_offset = @offset - @group.current_offset if delta_offset > 0 buffer << " fires in #{delta_offset} seconds" else buffer << " fired #{delta_offset.abs} seconds ago" end buffer << ", recurs every #{interval}" if recurring end buffer << ">" return buffer end end end ================================================ FILE: lib/timers/version.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2016, by Tony Arcieri. # Copyright, 2014-2022, by Samuel Williams. # Copyright, 2015, by Donovan Keme. module Timers VERSION = "4.4.0" end ================================================ FILE: lib/timers/wait.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. # Copyright, 2015, by Utenmiki. # Copyright, 2015, by Donovan Keme. require_relative "interval" module Timers # An exclusive, monotonic timeout class. class Wait def self.for(duration, &block) if duration timeout = new(duration) timeout.while_time_remaining(&block) else # If there is no "duration" to wait for, we wait forever. loop do yield(nil) end end end def initialize(duration) @duration = duration @remaining = true end attr_reader :duration attr_reader :remaining # Yields while time remains for work to be done: def while_time_remaining @interval = Interval.new @interval.start yield @remaining while time_remaining? ensure @interval.stop @interval = nil end private def time_remaining? @remaining = (@duration - @interval.to_f) @remaining > 0 end end end ================================================ FILE: lib/timers.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2016, by Tony Arcieri. # Copyright, 2012, by Ryan LeCompte. # Copyright, 2012, by Nicholas Evans. # Copyright, 2012, by Dimitrij Denissenko. # Copyright, 2013, by Chuck Remes. # Copyright, 2013, by Ron Evans. # Copyright, 2013, by Sean Gregory. # Copyright, 2013, by Utenmiki. # Copyright, 2013, by Jeremy Hinegardner. # Copyright, 2014, by Larry Lv. # Copyright, 2014, by Bruno Enten. # Copyright, 2014-2022, by Samuel Williams. # Copyright, 2014, by Mike Bourgeous. require_relative "timers/version" require_relative "timers/group" require_relative "timers/wait" ================================================ FILE: license.md ================================================ # MIT License Copyright, 2012-2017, by Tony Arcieri. Copyright, 2012, by Ryan LeCompte. Copyright, 2012, by Jesse Cooke. Copyright, 2012, by Nicholas Evans. Copyright, 2012, by Dimitrij Denissenko. Copyright, 2013, by Chuck Remes. Copyright, 2013, by Ron Evans. Copyright, 2013, by Sean Gregory. Copyright, 2013-2015, by Utenmiki. Copyright, 2013, by Jeremy Hinegardner. Copyright, 2014, by Larry Lv. Copyright, 2014, by Bruno Enten. Copyright, 2014-2025, by Samuel Williams. Copyright, 2014, by Mike Bourgeous. Copyright, 2014, by Klaus Trainer. Copyright, 2014, by Lin Jen-Shin. Copyright, 2014, by Lavir the Whiolet. Copyright, 2015-2016, by Donovan Keme. Copyright, 2015, by Tommy Ong Gia Phu. Copyright, 2015, by Will Jessop. Copyright, 2016, by Ryunosuke Sato. Copyright, 2016, by Atul Bhosale. Copyright, 2017, by Vít Ondruch. Copyright, 2017-2020, by Olle Jonsson. Copyright, 2020, by Tim Smith. Copyright, 2021, by Wander Hillen. Copyright, 2022, by Yoshiki Takagi. Copyright, 2023, by Peter Goldstein. Copyright, 2025, by Patrik Wenger. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: readme.md ================================================ # Timers Collections of one-shot and periodic timers, intended for use with event loops such as [async](https://github.com/socketry/async). [![Development Status](https://github.com/socketry/timers/workflows/Test/badge.svg)](https://github.com/socketry/timers/actions?workflow=Test) ## Installation Add this line to your application's Gemfile: ``` ruby gem 'timers' ``` And then execute: $ bundle Or install it yourself as: $ gem install timers ## Usage Create a new timer group with `Timers::Group.new`: ``` ruby require 'timers' timers = Timers::Group.new ``` Schedule a proc to run after 5 seconds with `Timers::Group#after`: ``` ruby five_second_timer = timers.after(5) { puts "Take five" } ``` The `five_second_timer` variable is now bound to a Timers::Timer object. To cancel a timer, use `Timers::Timer#cancel` Once you've scheduled a timer, you can wait until the next timer fires with `Timers::Group#wait`: ``` ruby # Waits 5 seconds timers.wait # The script will now print "Take five" ``` You can schedule a block to run periodically with `Timers::Group#every`: ``` ruby every_five_seconds = timers.every(5) { puts "Another 5 seconds" } loop { timers.wait } ``` You can also schedule a block to run immediately and periodically with `Timers::Group#now_and_every`: ``` ruby now_and_every_five_seconds = timers.now_and_every(5) { puts "Now and in another 5 seconds" } loop { timers.wait } ``` If you'd like another method to do the waiting for you, e.g. `Kernel.select`, you can use `Timers::Group#wait_interval` to obtain the amount of time to wait. When a timeout is encountered, you can fire all pending timers with `Timers::Group#fire`: ``` ruby loop do interval = timers.wait_interval ready_readers, ready_writers = select readers, writers, nil, interval if ready_readers || ready_writers # Handle IO ... else # Timeout! timers.fire end end ``` You can also pause and continue individual timers, or all timers: ``` ruby paused_timer = timers.every(5) { puts "I was paused" } paused_timer.pause 10.times { timers.wait } # will not fire paused timer paused_timer.resume 10.times { timers.wait } # will fire timer timers.pause 10.times { timers.wait } # will not fire any timers timers.resume 10.times { timers.wait } # will fire all timers ``` ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. ### Developer Certificate of Origin In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. ### Community Guidelines This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. ================================================ FILE: release.cert ================================================ -----BEGIN CERTIFICATE----- MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= -----END CERTIFICATE----- ================================================ FILE: test/timers/events.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. require "timers/events" describe Timers::Events do let(:events) {subject.new} it "should register an event" do fired = false callback = proc do |_time| fired = true end events.schedule(0.1, callback) expect(events.size).to be == 1 events.fire(0.15) expect(events.size).to be == 0 expect(fired).to be == true end it "should register events in order" do fired = [] times = [0.95, 0.1, 0.3, 0.5, 0.4, 0.2, 0.01, 0.9] times.each do |requested_time| callback = proc do |_time| fired << requested_time end events.schedule(requested_time, callback) end events.fire(0.5) expect(fired).to be == times.sort.first(6) events.fire(1.0) expect(fired).to be == times.sort end it "should fire events with the time they were fired at" do fired_at = :not_fired callback = proc do |time| # The time we actually were fired at: fired_at = time end events.schedule(0.5, callback) events.fire(1.0) expect(fired_at).to be == 1.0 end it "should flush cancelled events" do callback = proc{} 10.times do handle = events.schedule(0.1, callback) handle.cancel! end expect(events.size).to be == 1 end end ================================================ FILE: test/timers/group/cancel.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014, by Lin Jen-Shin. # Copyright, 2014-2016, by Tony Arcieri. # Copyright, 2014-2025, by Samuel Williams. require "timers/group" describe Timers::Group do let(:group) {subject.new} it "can cancel a timer" do fired = false timer = group.after(0.1) { fired = true } timer.cancel group.wait expect(fired).to be == false end it "should be able to cancel twice" do fired = false timer = group.after(0.1) { fired = true } 2.times do timer.cancel group.wait end expect(fired).to be == false end it "should be possble to reset after cancel" do fired = false timer = group.after(0.1) { fired = true } timer.cancel group.wait timer.reset group.wait expect(fired).to be == true end it "should cancel and remove one shot timers after they fire" do x = 0 Timers::Wait.for(2) do |_remaining| timer = group.every(0.2) { x += 1 } group.after(0.1) { timer.cancel } group.wait end expect(group.timers).to be(:empty?) expect(x).to be == 0 end with "#cancel" do it "should cancel all timers" do timers = 3.times.map do group.every(0.1) {} end expect(group.timers).not.to be(:empty?) group.cancel expect(group.timers).to be(:empty?) end end end ================================================ FILE: test/timers/group/every.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. # Copyright, 2015, by Tommy Ong Gia Phu. # Copyright, 2015, by Donovan Keme. require "timers/group" describe Timers::Group do let(:group) {subject.new} it "should fire several times" do result = [] group.every(0.7) { result << :a } group.every(2.3) { result << :b } group.every(1.3) { result << :c } group.every(2.4) { result << :d } Timers::Wait.for(2.5) do |remaining| group.wait if group.wait_interval < remaining end expect(result).to be == [:a, :c, :a, :a, :b, :d] end it "should fire immediately and then several times later" do result = [] group.every(0.7) { result << :a } group.every(2.3) { result << :b } group.now_and_every(1.3) { result << :c } group.now_and_every(2.4) { result << :d } Timers::Wait.for(2.5) do |remaining| group.wait if group.wait_interval < remaining end expect(result).to be == [:c, :d, :a, :c, :a, :a, :b, :d] end end ================================================ FILE: test/timers/group/pause.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2025, by Samuel Williams. require "timers/group" describe Timers::Group do let(:group) {subject.new} let(:interval) {0.01} def before @fired = false @timer = group.after(interval) {@fired = true} @fired2 = false @timer2 = group.after(interval) {@fired2 = true} super end it "does not fire when paused" do @timer.pause group.wait expect(@fired).to be == false end it "fires when continued after pause" do @timer.pause group.wait @timer.resume sleep(interval) group.wait expect(@fired).to be == true end it "can pause all timers at once" do group.pause group.wait expect(@fired).to be == false expect(@fired2).to be == false end it "can continue all timers at once" do group.pause group.wait group.resume sleep(interval + TIMER_QUANTUM) group.wait expect(@fired).to be == true expect(@fired2).to be == true end it "can fire the timer directly" do @timer.pause group.wait expect(@fired).not.to be == true @timer.resume expect(@fired).not.to be == true @timer.fire expect(@fired).to be == true end end ================================================ FILE: test/timers/group.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2012-2017, by Tony Arcieri. # Copyright, 2012, by Jesse Cooke. # Copyright, 2012, by Dimitrij Denissenko. # Copyright, 2013, by Chuck Remes. # Copyright, 2013, by Ron Evans. # Copyright, 2013, by Sean Gregory. # Copyright, 2013-2014, by Utenmiki. # Copyright, 2013, by Jeremy Hinegardner. # Copyright, 2014, by Bruno Enten. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2017, by Vít Ondruch. require "timers/group" require "timer_quantum" describe Timers::Group do let(:group) {subject.new} with "#wait" do it "calls the wait block with nil" do called = false group.wait do |interval| expect(interval).to be_nil called = true end expect(called).to be == true end it "calls the wait block with an interval" do called = false fired = false group.after(0.1) { fired = true } group.wait do |interval| expect(interval).to be_within(TIMER_QUANTUM).of(0.1) called = true sleep 0.2 end expect(called).to be == true expect(fired).to be == true end it "repeatedly calls the wait block if it sleeps less than the interval" do called = 0 fired = false group.after(0.1) { fired = true } group.wait do |interval| called += 1 sleep(0.01) end expect(called).to be > 1 expect(fired).to be == true end end it "sleeps until the next timer" do interval = 0.1 fired = false group.after(interval) {fired = true} group.wait expect(fired).to be == true end it "fires instantly when next timer is in the past" do fired = false group.after(TIMER_QUANTUM) { fired = true } sleep(TIMER_QUANTUM * 2) group.wait expect(fired).to be == true end it "calculates the interval until the next timer should fire" do interval = 0.1 group.after(interval) expect(group.wait_interval).to be_within(TIMER_QUANTUM).of interval sleep(interval) expect(group.wait_interval).to be <= 0 end it "fires timers in the correct order" do result = [] group.after(TIMER_QUANTUM * 2) { result << :two } group.after(TIMER_QUANTUM * 3) { result << :three } group.after(TIMER_QUANTUM * 1) { result << :one } sleep(TIMER_QUANTUM * 4) group.fire expect(result).to be == [:one, :two, :three] end it "raises TypeError if given an invalid time" do expect do group.after(nil) { nil } end.to raise_exception(TypeError) end with "#now_and_after" do it "fires the timer immediately" do result = [] group.now_and_after(TIMER_QUANTUM * 2) { result << :foo } expect(result).to be == [:foo] end it "fires the timer at the correct time" do result = [] group.now_and_after(TIMER_QUANTUM * 2) { result << :foo } group.wait expect(result).to be == [:foo, :foo] end end with "recurring timers" do it "continues to fire the timers at each interval" do result = [] group.every(TIMER_QUANTUM * 2) { result << :foo } sleep TIMER_QUANTUM * 3 group.fire expect(result).to be == [:foo] sleep TIMER_QUANTUM * 5 group.fire expect(result).to be == [:foo, :foo] end end it "calculates the proper interval to wait until firing" do interval_ms = 25 group.after(interval_ms / 1000.0) expect(group.wait_interval).to be_within(TIMER_QUANTUM).of(interval_ms / 1000.0) end with "delay timer" do it "adds appropriate amount of time to timer" do timer = group.after(10) timer.delay(5) expect(timer.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(15) end end with "delay timer collection" do it "delay on set adds appropriate amount of time to all timers" do timer = group.after(10) timer2 = group.after(20) group.delay(5) expect(timer.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(15) expect(timer2.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(25) end end with "on delaying a timer" do it "fires timers in the correct order" do result = [] group.after(TIMER_QUANTUM * 2) { result << :two } group.after(TIMER_QUANTUM * 3) { result << :three } first = group.after(TIMER_QUANTUM * 1) { result << :one } first.delay(TIMER_QUANTUM * 3) sleep TIMER_QUANTUM * 5 group.fire expect(result).to be == [:two, :three, :one] end end with "#inspect" do it "before firing" do fired = false timer = group.after(TIMER_QUANTUM * 5) { fired = true } timer.pause expect(fired).not.to be == true expect(timer.inspect).to be =~ /\A#\Z/ end it "after firing" do fired = false timer = group.after(TIMER_QUANTUM) { fired = true } group.wait expect(fired).to be == true expect(timer.inspect).to be =~/\A#\Z/ end it "recurring firing" do result = [] timer = group.every(TIMER_QUANTUM) { result << :foo } group.wait expect(result).to be(:any?) regex = /\A#\Z/ expect(timer.inspect).to be =~ regex end end with "#fires_in" do let(:interval) {0.01} with "recurring timer" do let(:timer) {group.every(interval){true}} it "calculates the interval until the next fire if it's recurring" do expect(timer.fires_in).to be_within(TIMER_QUANTUM).of(interval) end end with "non-recurring timer" do let(:timer) {group.after(interval){true}} it "calculates the interval until the next fire if it hasn't already fired" do expect(timer.fires_in).to be_within(TIMER_QUANTUM).of(interval) end it "calculates the interval since last fire if already fired" do # Create the timer: timer group.wait sleep(TIMER_QUANTUM) expect(timer.fires_in).to be < 0.0 end end end end ================================================ FILE: test/timers/performance.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. # Copyright, 2015, by Donovan Keme. # Copyright, 2021, by Wander Hillen. # Event based timers: # Serviced 31812 events in 2.39075272 seconds, 13306.320832794887 e/s. # Thread ID: 7336700 # Fiber ID: 30106340 # Total: 2.384043 # Sort by: self_time # %self total self wait child calls name # 13.48 0.510 0.321 0.000 0.189 369133 Timers::Events::Handle#<=> # 8.12 0.194 0.194 0.000 0.000 427278 Timers::Events::Handle#to_f # 4.55 0.109 0.109 0.000 0.000 427278 Float#<=> # 4.40 1.857 0.105 0.000 1.752 466376 *Timers::Events#bsearch # 4.30 0.103 0.103 0.000 0.000 402945 Float#to_f # 2.65 0.063 0.063 0.000 0.000 33812 Array#insert # 2.64 1.850 0.063 0.000 1.787 33812 Timers::Events#schedule # 2.40 1.930 0.057 0.000 1.873 33812 Timers::Timer#reset # 1.89 1.894 0.045 0.000 1.849 31812 Timers::Timer#fire # 1.69 1.966 0.040 0.000 1.926 31812 Timers::Events::Handle#fire # 1.35 0.040 0.032 0.000 0.008 33812 Timers::Events::Handle#initialize # 1.29 0.044 0.031 0.000 0.013 44451 Timers::Group#current_offset # SortedSet based timers: # Serviced 32516 events in 66.753277275 seconds, 487.1072288781219 e/s. # Thread ID: 15995640 # Fiber ID: 38731780 # Total: 66.716394 # Sort by: self_time # %self total self wait child calls name # 54.73 49.718 36.513 0.000 13.205 57084873 Timers::Timer#<=> # 23.74 65.559 15.841 0.000 49.718 32534 Array#sort! # 19.79 13.205 13.205 0.000 0.000 57084873 Float#<=> # Max out events performance (on my computer): # Serviced 1142649 events in 11.194903921 seconds, 102068.70405115146 e/s. require "timers/group" describe Timers::Group do let(:group) {subject.new} with "profiler" do if defined? RubyProf def before # Running RubyProf makes the code slightly slower. RubyProf.start puts "*** Running with RubyProf reduces performance ***" super end def after super if RubyProf.running? # file = arg.metadata[:description].gsub(/\s+/, '-') result = RubyProf.stop printer = RubyProf::FlatPrinter.new(result) printer.print($stderr, min_percent: 1.0) end end end it "runs efficiently" do result = [] range = (1..500) duration = 2.0 total = 0 range.each do |index| offset = index.to_f / range.max total += (duration / offset).floor group.every(index.to_f / range.max, :strict) { result << index } end group.wait while result.size < total rate = result.size.to_f / group.current_offset inform "Serviced #{result.size} events in #{group.current_offset} seconds, #{rate} e/s." expect(group.current_offset).to be_within(20).percent_of(duration) end end it "runs efficiently at high volume" do results = [] range = (1..300) groups = (1..20) duration = 2.0 timers = [] @mutex = Mutex.new start = Time.now groups.each do timers << Thread.new do result = [] timer = Timers::Group.new total = 0 range.each do |index| offset = index.to_f / range.max total += (duration / offset).floor timer.every(index.to_f / range.max, :strict) { result << index } end timer.wait while result.size < total @mutex.synchronize { results += result } end end timers.each { |t| t.join } finish = Time.now runtime = finish - start rate = results.size.to_f / runtime inform "Serviced #{results.size} events in #{runtime} seconds, #{rate} e/s; across #{groups.max} timers." expect(runtime).to be_within(20).percent_of(duration) end it "copes with very large amounts of timers" do # This spec tries to emulate (as best as possible) the timer characteristics of the # following scenario: # - a fairly busy Falcon server serving a constant stream of request that spend most of their time # in a long database call. Both the web request and the db call have a timeout attached # - there will already exist a lot of timers in the queue and more are added all the time # - the server is assumed to be busy so there are "always" new requests waiting to be accept()-ed # and thus the server spends relatively little time actually sleeping and most of its time in # either the reactor or an active fiber. # - On each loop of the reactor it will run any fibers in the ready queue, accept any waiting # requests on the server socket and then call wait_interval to see if there are any expired # timeouts that need to be handled. # Result for PriorityHeap based timer queue: Inserted 20k timers in 0.055050924 seconds # Result for Array based timer queue: Inserted 20k timers in 0.141001845 seconds results = [] # Prefill the timer queue with a lot of timers in the semidistant future 20000.times do group.after(10) { results << "yay!" } end # add one timer which is done immediately, to get the pending array into the queue group.after(-1) { results << "I am first!" } group.wait expect(results.size).to be == 1 expect(results.first).to be == "I am first!" # 20k extra requests come in and get added into the queue start = Time.now 20000.times do # add new timer to the queue (later than all the others so far) group.after(15) { result << "yay again!" } # wait_interval in the reactor loop group.wait_interval() end expect(group.events.size).to be == 40_000 inform "Inserted 20k timers in #{Time.now - start} seconds" end end ================================================ FILE: test/timers/priority_heap.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021, by Wander Hillen. # Copyright, 2021-2025, by Samuel Williams. require "timers/priority_heap" describe Timers::PriorityHeap do let(:priority_heap) {subject.new} with "empty heap" do it "should return nil when the first element is requested" do expect(priority_heap.peek).to be_nil end it "should return nil when the first element is extracted" do expect(priority_heap.pop).to be_nil end it "should report its size as zero" do expect(priority_heap.size).to be(:zero?) end end it "returns the same element after inserting a single element" do priority_heap.push(1) expect(priority_heap.size).to be == 1 expect(priority_heap.pop).to be == 1 expect(priority_heap.size).to be(:zero?) end it "should return inserted elements in ascending order no matter the insertion order" do (1..10).to_a.shuffle.each do |e| priority_heap.push(e) end expect(priority_heap.size).to be == 10 expect(priority_heap.peek).to be == 1 result = [] 10.times do result << priority_heap.pop end expect(result.size).to be == 10 expect(priority_heap.size).to be(:zero?) expect(result.sort).to be == result end with "maintaining the heap invariant" do it "for empty heaps" do expect(priority_heap).to be(:valid?) end it "for heap of size 1" do priority_heap.push(123) expect(priority_heap).to be(:valid?) end # Exhaustive testing of all permutations of [1..6] it "for all permutations of size 6" do [1,2,3,4,5,6].permutation do |arr| priority_heap.clear! arr.each { |e| priority_heap.push(e) } expect(priority_heap).to be(:valid?) end end # A few examples with more elements (but not ALL permutations) it "for larger amounts of values" do 5.times do priority_heap.clear! (1..1000).to_a.shuffle.each { |e| priority_heap.push(e) } expect(priority_heap).to be(:valid?) end end # What if we insert several of the same item along with others? it "with several elements of the same value" do test_values = (1..10).to_a + [4] * 5 test_values.each { |e| priority_heap.push(e) } expect(priority_heap).to be(:valid?) end end end ================================================ FILE: test/timers/strict.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. require "timers/group" require "timer_quantum" describe Timers::Group do let(:group) {subject.new} it "should not diverge too much" do fired = :not_fired_yet count = 0 quantum = 0.01 start_offset = group.current_offset Timers::Timer.new(group, quantum, :strict, start_offset) do |offset| fired = offset count += 1 end iterations = 100 group.wait while count < iterations # In my testing on the JVM, without the :strict recurring, I noticed 60ms of error here. expect(fired - start_offset).to be_within(quantum + TIMER_QUANTUM).of(iterations * quantum) end it "should only fire 0-interval timer once per iteration" do count = 0 start_offset = group.current_offset Timers::Timer.new(group, 0, :strict, start_offset) do |offset, timer| count += 1 end group.wait expect(count).to be == 1 end end ================================================ FILE: test/timers/timer.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Patrik Wenger. # Copyright, 2025, by Samuel Williams. require "timers/timer" describe Timers::Timer do let(:group) {Timers::Group.new} it "should return the block value when fired" do timer = group.after(10) {:foo} result = timer.fire expect(result).to be == :foo end end ================================================ FILE: test/timers/wait.rb ================================================ # frozen_string_literal: true # Released under the MIT License. # Copyright, 2014-2025, by Samuel Williams. # Copyright, 2014-2016, by Tony Arcieri. require "timers/wait" require "timer_quantum" describe Timers::Wait do let(:interval) {0.1} let(:repeats) {10} it "repeats until timeout expired" do timeout = Timers::Wait.new(interval*repeats) count = 0 previous_remaining = nil timeout.while_time_remaining do |remaining| if previous_remaining expect(remaining).to be_within(TIMER_QUANTUM).of(previous_remaining - interval) end previous_remaining = remaining count += 1 sleep(interval) end expect(count).to be == repeats end it "yields results as soon as possible" do timeout = Timers::Wait.new(5) result = timeout.while_time_remaining do |_remaining| break :done end expect(result).to be == :done end with "#for" do with "no duration" do it "waits forever" do count = 0 Timers::Wait.for(nil) do count += 1 break if count > 10 end expect(count).to be > 10 end end end end ================================================ FILE: timers.gemspec ================================================ # frozen_string_literal: true require_relative "lib/timers/version" Gem::Specification.new do |spec| spec.name = "timers" spec.version = Timers::VERSION spec.summary = "Pure Ruby one-shot and periodic timers." spec.authors = ["Tony Arcieri", "Samuel Williams", "Donovan Keme", "Wander Hillen", "Utenmiki", "Jeremy Hinegardner", "Sean Gregory", "Chuck Remes", "Olle Jonsson", "Ron Evans", "Tommy Ong Gia Phu", "Larry Lv", "Lin Jen-Shin", "Ryunosuke Sato", "Atul Bhosale", "Bruno Enten", "Dimitrij Denissenko", "Jesse Cooke", "Klaus Trainer", "Lavir the Whiolet", "Mike Bourgeous", "Nicholas Evans", "Patrik Wenger", "Peter Goldstein", "Ryan LeCompte", "Tim Smith", "Vít Ondruch", "Will Jessop", "Yoshiki Takagi"] spec.license = "MIT" spec.cert_chain = ["release.cert"] spec.signing_key = File.expand_path("~/.gem/release.pem") spec.homepage = "https://github.com/socketry/timers" spec.metadata = { "source_code_uri" => "https://github.com/socketry/timers.git", } spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) spec.required_ruby_version = ">= 3.1" end