Module: Aidp::Concurrency::Wait

Defined in:
lib/aidp/concurrency/wait.rb

Overview

Deterministic condition waiting with timeouts and intervals.

Replaces sleep-based polling loops with proper timeout enforcement and early exit on condition satisfaction.

Examples:

Wait for file to exist

Wait.until(timeout: 30, interval: 0.2) { File.exist?("/tmp/ready") }

Wait for port to open

Wait.until(timeout: 60, interval: 1) do
  TCPSocket.new("localhost", 8080).close rescue false
end

Custom error message

Wait.until(timeout: 10, message: "Service failed to start") do
  service_ready?
end

Class Method Summary collapse

Class Method Details

.for_file(path, timeout: nil, interval: nil) ⇒ String

Wait for a file to exist.

Parameters:

  • path (String)

    File path to check

  • timeout (Float) (defaults to: nil)

    Maximum seconds to wait

  • interval (Float) (defaults to: nil)

    Seconds between checks

Returns:

  • (String)

    The file path if it exists

Raises:



76
77
78
79
80
81
82
83
# File 'lib/aidp/concurrency/wait.rb', line 76

def for_file(path, timeout: nil, interval: nil)
  self.until(
    timeout: timeout,
    interval: interval,
    message: "File not found: #{path} (waited #{timeout || Concurrency.configuration.default_timeout}s)"
  ) { File.exist?(path) }
  path
end

.for_port(host, port, timeout: nil, interval: nil) ⇒ Boolean

Wait for a TCP port to be open.

Parameters:

  • host (String)

    Hostname or IP

  • port (Integer)

    Port number

  • timeout (Float) (defaults to: nil)

    Maximum seconds to wait

  • interval (Float) (defaults to: nil)

    Seconds between checks

Returns:

  • (Boolean)

    true if port is open

Raises:



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/aidp/concurrency/wait.rb', line 93

def for_port(host, port, timeout: nil, interval: nil)
  require "socket"
  self.until(
    timeout: timeout,
    interval: interval,
    message: "Port #{host}:#{port} not open (waited #{timeout || Concurrency.configuration.default_timeout}s)"
  ) do
    socket = TCPSocket.new(host, port)
    socket.close
    true
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT
    false
  end
end

.for_process_exit(pid, timeout: nil, interval: nil) ⇒ Process::Status

Wait for a process to exit.

Parameters:

  • pid (Integer)

    Process ID

  • timeout (Float) (defaults to: nil)

    Maximum seconds to wait

  • interval (Float) (defaults to: nil)

    Seconds between checks

Returns:

  • (Process::Status)

    The process exit status

Raises:



115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/aidp/concurrency/wait.rb', line 115

def for_process_exit(pid, timeout: nil, interval: nil)
  status = nil
  self.until(
    timeout: timeout,
    interval: interval,
    message: "Process #{pid} did not exit (waited #{timeout || Concurrency.configuration.default_timeout}s)"
  ) do
    _, status = Process.waitpid2(pid, Process::WNOHANG)
    status # Returns truthy when process has exited
  end
  status
end

.until(timeout: nil, interval: nil, message: nil) { ... } ⇒ Object

Wait until a condition becomes true, with timeout and interval polling.

Examples:

result = Wait.until(timeout: 5, interval: 0.1) { expensive_check() }

Parameters:

  • timeout (Float) (defaults to: nil)

    Maximum seconds to wait (default: from config)

  • interval (Float) (defaults to: nil)

    Seconds between condition checks (default: from config)

  • message (String) (defaults to: nil)

    Custom error message on timeout

Yields:

  • Block that returns truthy when condition is met

Returns:

  • (Object)

    The truthy value from the block

Raises:



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/aidp/concurrency/wait.rb', line 37

def until(timeout: nil, interval: nil, message: nil, &block)
  timeout ||= Concurrency.configuration.default_timeout
  interval ||= Concurrency.configuration.default_interval
  message ||= "Condition not met within #{timeout}s"

  raise ArgumentError, "Block required" unless block_given?

  start_time = monotonic_time
  deadline = start_time + timeout
  elapsed = 0.0

  loop do
    result = block.call
    if result
      log_wait_completion(elapsed) if should_log_wait?(elapsed)
      return result
    end

    elapsed = monotonic_time - start_time
    remaining = deadline - monotonic_time

    if remaining <= 0
      log_timeout(elapsed, message)
      raise Concurrency::TimeoutError, message
    end

    # Sleep for interval or remaining time, whichever is shorter
    sleep_duration = [interval, remaining].min
    sleep(sleep_duration) if sleep_duration > 0
  end
end