Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/async.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

require_relative "async/version"
require_relative "async/reactor"
require_relative "async/loop"

require_relative "kernel/async"
require_relative "kernel/sync"
Expand Down
35 changes: 35 additions & 0 deletions lib/async/loop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "console"

module Async
# A helper for running loops at aligned intervals.
module Loop
# A robust loop that executes a block at aligned intervals.
#
# The alignment is modulo the current clock in seconds.
#
# If an error occurs during the execution of the block, it is logged and the loop continues.
#
# @parameter interval [Integer] The interval in seconds between executions of the block.
def self.run(interval: 60, &block)
while true
# Compute the wait time to the next interval:
wait = interval - (Time.now.to_f % interval)
if wait.positive?
# Sleep until the next interval boundary:
sleep(wait)
end

begin
yield
rescue => error
Console.error(self, "Loop error:", error)
end
end
end
end
end
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- Introduce `Async::Loop` for robust, time-aligned loops.

## v2.36.0

- Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
Expand Down
66 changes: 66 additions & 0 deletions test/async/loop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "async/loop"
require "sus/fixtures/console"

describe Async::Loop do
include Sus::Fixtures::Console::CapturedLogger

with ".run" do
it "invokes the block at aligned intervals" do
iterations = 0
thread = Thread.new do
Async::Loop.run(interval: 0.1) do
iterations += 1
end
end

sleep(0.35)
expect(iterations).to be >= 2
ensure
thread.kill
thread.join
end

it "uses the given interval" do
iterations = 0
interval = 0.05
thread = Thread.new do
Async::Loop.run(interval: interval) do
iterations += 1
end
end

sleep(0.2)
expect(iterations).to be >= 2
ensure
thread.kill
thread.join
end

it "continues after an error and logs it" do
iterations = 0
thread = Thread.new do
Async::Loop.run(interval: 0.05) do
iterations += 1
raise "test error" if iterations == 1
end
end

# Allow first iteration (raises), then at least one more (succeeds)
sleep(0.2)
expect(iterations).to be >= 2
expect_console.to have_logged(
severity: be == :error,
subject: be_equal(Async::Loop),
message: be =~ /Loop error:/
)
ensure
thread.kill
thread.join
end
end
end
Loading