54 lines
1.6 KiB
Ruby
54 lines
1.6 KiB
Ruby
require "json"
|
|
require_relative "result"
|
|
require_relative "http_client"
|
|
require_relative "log"
|
|
|
|
module Relay
|
|
# Delivers one webhook payload to one endpoint, with retries.
|
|
#
|
|
# Failed attempts back off exponentially (1s, 2s, 4s, 8s, 16s) and give up
|
|
# after MAX_ATTEMPTS; receivers throttle by IP, so attempts are never
|
|
# retried back-to-back. A delivery that exhausts its attempts is appended
|
|
# to the dead-letter file for manual replay.
|
|
class Delivery
|
|
MAX_ATTEMPTS = 5
|
|
BASE_BACKOFF = 1 # seconds; doubles each attempt
|
|
|
|
def self.attempt_budget = MAX_ATTEMPTS
|
|
|
|
def initialize(client: HttpClient.new, log:, dead_letter_path:)
|
|
@client = client
|
|
@log = log
|
|
@dead_letter_path = dead_letter_path
|
|
end
|
|
|
|
def deliver(url, payload)
|
|
body = JSON.generate(payload)
|
|
last_error = nil
|
|
|
|
MAX_ATTEMPTS.times do |attempt|
|
|
sleep(BASE_BACKOFF * (2**(attempt - 1))) if attempt.positive?
|
|
@log.record("attempt", "#{url} ##{attempt + 1}")
|
|
result = @client.post(url, body)
|
|
if result.ok?
|
|
@log.record("delivered", url)
|
|
return Result.ok(result.value)
|
|
end
|
|
last_error = result.error
|
|
@log.record("failed", "#{url}: #{last_error}")
|
|
end
|
|
|
|
dead_letter(url, payload, last_error)
|
|
Result.err("gave up after #{MAX_ATTEMPTS} attempts: #{last_error}")
|
|
end
|
|
|
|
private
|
|
|
|
def dead_letter(url, payload, error)
|
|
entry = { "url" => url, "payload" => payload, "error" => error }
|
|
File.open(@dead_letter_path, "a") { |f| f.puts(JSON.generate(entry)) }
|
|
@log.record("dead-lettered", url)
|
|
end
|
|
end
|
|
end
|