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 <nevans@410labs.com>
Ron Evans <ron.evans@gmail.com>
Sean Gregory <skinnyjames@pigadmirersclub.net>
Utenmiki <utenmiki@gmail.com>
Donovan Keme <code@extremist.digital>
Donovan Keme <de@freed.network>
Donovan Keme <digitalextremist@users.noreply.github.com>
Utenmiki <takiy33@gmail.com>
Tommy Ong Gia Phu <tommyogp@gmail.com>
Ryunosuke Sato <tricknotes.rs@gmail.com>
================================================
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 <https://en.wikipedia.org/wiki/Binary_heap> 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).
[](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#<Timers::Timer:0x[\da-f]+ fires in [-\.\de]+ seconds>\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#<Timers::Timer:0x[\da-f]+ fired [-\.\de]+ seconds ago>\Z/
end
it "recurring firing" do
result = []
timer = group.every(TIMER_QUANTUM) { result << :foo }
group.wait
expect(result).to be(:any?)
regex = /\A#<Timers::Timer:0x[\da-f]+ fires in [-\.\de]+ seconds, recurs every #{TIMER_QUANTUM}>\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
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
SYMBOL INDEX (77 symbols across 10 files)
FILE: fixtures/timer_quantum.rb
class TimerQuantum (line 6) | class TimerQuantum
method resolve (line 7) | def self.resolve
method to_f (line 11) | def to_f
method precision (line 17) | def precision
method measure_host_precision (line 21) | def measure_host_precision(repeats: 100, duration: 0.01)
method now (line 41) | def now
FILE: lib/timers/events.rb
type Timers (line 14) | module Timers
class Events (line 16) | class Events
class Handle (line 18) | class Handle
method initialize (line 21) | def initialize(time, callback)
method cancel! (line 30) | def cancel!
method cancelled? (line 37) | def cancelled?
method <=> (line 41) | def <=> other
method fire (line 46) | def fire(time)
method initialize (line 51) | def initialize
method schedule (line 59) | def schedule(time, callback)
method first (line 70) | def first
method size (line 80) | def size
method fire (line 85) | def fire(time)
method merge! (line 97) | def merge!
method flush! (line 105) | def flush!
FILE: lib/timers/group.rb
type Timers (line 16) | module Timers
class Group (line 18) | class Group
method initialize (line 24) | def initialize
method after (line 45) | def after(interval, &block)
method now_and_after (line 51) | def now_and_after(interval, &block)
method every (line 58) | def every(interval, recur = true, &block)
method now_and_every (line 64) | def now_and_every(interval, recur = true, &block)
method wait (line 72) | def wait
method wait_interval (line 94) | def wait_interval(offset = current_offset)
method fire (line 101) | def fire(offset = current_offset)
method pause (line 106) | def pause
method resume (line 111) | def resume
method delay (line 118) | def delay(seconds)
method cancel (line 125) | def cancel
method current_offset (line 130) | def current_offset
FILE: lib/timers/interval.rb
type Timers (line 6) | module Timers
class Interval (line 8) | class Interval
method initialize (line 10) | def initialize
method start (line 15) | def start
method stop (line 21) | def stop
method to_f (line 29) | def to_f
method duration (line 33) | def duration
method now (line 37) | def now
FILE: lib/timers/priority_heap.rb
type Timers (line 7) | module Timers
class PriorityHeap (line 12) | class PriorityHeap
method initialize (line 13) | def initialize
method peek (line 21) | def peek
method size (line 26) | def size
method pop (line 32) | def pop
method push (line 61) | def push(element)
method clear! (line 74) | def clear!
method valid? (line 80) | def valid?
method bubble_up (line 92) | def bubble_up(index)
method bubble_down (line 108) | def bubble_down(index)
FILE: lib/timers/timer.rb
type Timers (line 11) | module Timers
class Timer (line 17) | class Timer
method initialize (line 21) | def initialize(group, interval, recurring = false, offset = nil, &bl...
method paused? (line 34) | def paused?
method pause (line 38) | def pause
method resume (line 48) | def resume
method delay (line 60) | def delay(seconds)
method cancel (line 69) | def cancel
method reset (line 81) | def reset(offset = @group.current_offset)
method fire (line 96) | def fire(offset = @group.current_offset)
method fires_in (line 114) | def fires_in
method inspect (line 119) | def inspect
FILE: lib/timers/version.rb
type Timers (line 8) | module Timers
FILE: lib/timers/wait.rb
type Timers (line 11) | module Timers
class Wait (line 13) | class Wait
method for (line 14) | def self.for(duration, &block)
method initialize (line 27) | def initialize(duration)
method while_time_remaining (line 36) | def while_time_remaining
method time_remaining? (line 48) | def time_remaining?
FILE: test/timers/group/pause.rb
function before (line 12) | def before
FILE: test/timers/performance.rb
function before (line 54) | def before
function after (line 62) | def after
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (60K chars).
[
{
"path": ".editorconfig",
"chars": 105,
"preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 2\n\n[*.{yml,yaml}]\nindent_style = space\nindent_size = 2\n"
},
{
"path": ".git-blame-ignore-revs",
"chars": 41,
"preview": "9180068e70c5b5c1fdb9a6c47f4d8f2553fc7104\n"
},
{
"path": ".github/workflows/documentation-coverage.yaml",
"chars": 450,
"preview": "name: Documentation Coverage\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n CO"
},
{
"path": ".github/workflows/documentation.yaml",
"chars": 1138,
"preview": "name: Documentation\n\non:\n push:\n branches:\n - main\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment "
},
{
"path": ".github/workflows/rubocop.yaml",
"chars": 376,
"preview": "name: RuboCop\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n\njobs:\n check:\n "
},
{
"path": ".github/workflows/test-coverage.yaml",
"chars": 1181,
"preview": "name: Test Coverage\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n COVERAGE: P"
},
{
"path": ".github/workflows/test.yaml",
"chars": 966,
"preview": "name: Test\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n\njobs:\n test:\n nam"
},
{
"path": ".gitignore",
"chars": 52,
"preview": "/.bundle/\n/pkg/\n/gems.locked\n/.covered.db\n/external\n"
},
{
"path": ".mailmap",
"chars": 381,
"preview": "Nicholas Evans <nevans@410labs.com>\nRon Evans <ron.evans@gmail.com>\nSean Gregory <skinnyjames@pigadmirersclub.net>\nUtenm"
},
{
"path": ".rubocop.yml",
"chars": 858,
"preview": "AllCops:\n DisabledByDefault: true\n\nLayout/IndentationStyle:\n Enabled: true\n EnforcedStyle: tabs\n\nLayout/InitialIndent"
},
{
"path": "config/sus.rb",
"chars": 153,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nrequire \"c"
},
{
"path": "fixtures/timer_quantum.rb",
"chars": 943,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nclass Time"
},
{
"path": "gems.rb",
"chars": 483,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2"
},
{
"path": "lib/timers/events.rb",
"chars": 2453,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2022, by Samuel Williams.\n# Copyright"
},
{
"path": "lib/timers/group.rb",
"chars": 3212,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "lib/timers/interval.rb",
"chars": 646,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2018-2022, by Samuel Williams.\n\nmodule Tim"
},
{
"path": "lib/timers/priority_heap.rb",
"chars": 4221,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2021, by Wander Hillen.\n# Copyright, 2021-"
},
{
"path": "lib/timers/timer.rb",
"chars": 3244,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "lib/timers/version.rb",
"chars": 224,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2"
},
{
"path": "lib/timers/wait.rb",
"chars": 1036,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "lib/timers.rb",
"chars": 650,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2"
},
{
"path": "license.md",
"chars": 2130,
"preview": "# MIT License\n\nCopyright, 2012-2017, by Tony Arcieri. \nCopyright, 2012, by Ryan LeCompte. \nCopyright, 2012, by Jesse C"
},
{
"path": "readme.md",
"chars": 3268,
"preview": "# Timers\n\nCollections of one-shot and periodic timers, intended for use with event loops such as [async](https://github."
},
{
"path": "release.cert",
"chars": 1740,
"preview": "-----BEGIN CERTIFICATE-----\nMIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11\nZWwud2lsbGlhbXMxHTAbBgoJkia"
},
{
"path": "test/timers/events.rb",
"chars": 1376,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "test/timers/group/cancel.rb",
"chars": 1364,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014, by Lin Jen-Shin.\n# Copyright, 2014-2"
},
{
"path": "test/timers/group/every.rb",
"chars": 1068,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "test/timers/group/pause.rb",
"chars": 1201,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nrequire \"t"
},
{
"path": "test/timers/group.rb",
"chars": 5963,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2017, by Tony Arcieri.\n# Copyright, 2"
},
{
"path": "test/timers/performance.rb",
"chars": 5918,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "test/timers/priority_heap.rb",
"chars": 2240,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2021, by Wander Hillen.\n# Copyright, 2021-"
},
{
"path": "test/timers/strict.rb",
"chars": 1012,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "test/timers/timer.rb",
"chars": 374,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2025, by Patrik Wenger.\n# Copyright, 2025,"
},
{
"path": "test/timers/wait.rb",
"chars": 1092,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "timers.gemspec",
"chars": 1114,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/timers/version\"\n\nGem::Specification.new do |spec|\n\tspec.name = \"tim"
}
]
About this extraction
This page contains the full source code of the socketry/timers GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (51.4 KB), approximately 16.7k tokens, and a symbol index with 77 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.