Class: Hook0::Client
- Inherits:
-
Object
- Object
- Hook0::Client
- Defined in:
- lib/hook0/client.rb
Overview
The Hook0 client, built once and shared wherever an application sends events.
Every event is sent under an identifier this client knows: the one set on the Event, or a UUIDv7 it generates when the event carries none. Passing none does not mean the identifier comes from Hook0 — the value comes from here, travels with the request, and is what #send_event answers.
That is what makes retrying safe. Hook0 keys events on that identifier, so a request repeated
after a network failure or a server error ingests the event once rather than twice; without a
client-chosen identifier, a repeated request would create a second event and deliver it to every
subscriber. It also gives the answer to a retry its meaning: EventAlreadyIngested in reply to
a repeated request says an earlier attempt of that same send reached the API, so the send
succeeded. The same answer to a first attempt is a genuine conflict and is reported as one.
Only what could end differently is retried: a request that got no answer, a server error, and an instance saying it is being reached faster than it accepts. What the API refuses outright — a quota that is spent, a payload it will not read — is reported as is, since repeating it would only spend the same round trip again. The verdict for every problem the API can report is written down in the conformance corpus committed beside this gem, which the suite here reads.
A send is bounded on five axes, each of them the caller's to set: the size of the payload, which is refused before a socket is opened; how long one attempt is given; how many attempts are made; how long a single wait between them may be; and how long every wait of one send may add up to.
Constant Summary collapse
- ALREADY_INGESTED =
The identifier Hook0 gives the problem it answers when an event identifier is already taken.
"EventAlreadyIngested"- RATE_LIMITED =
The identifier Hook0 gives the problem it answers when requests are reaching the instance faster than it accepts them.
It shares its status with the quota problems, and is the only one of them worth repeating: a quota clears when a plan changes or a day turns, neither of which happens inside the seconds a send is given, while pacing clears on its own and the answer says when.
"RateLimited"- CONFLICT =
What Hook0 answers when the event identifier a request carries is already taken.
409- PACED =
What Hook0 answers both when a quota is spent and when requests are coming in faster than the instance accepts them. Which of the two it is only the problem the body names can say, which is why this status alone decides nothing.
429- LOWEST_SERVER_ERROR =
First status saying the failure is on Hook0's side, and so could clear on its own.
500- DELAY_HEADER =
What the API names the delay before the request becomes servable in, in whole seconds.
"retry-after"- MAX_DELAY_HEADER_BYTES =
Longest value of that header read, and the largest delay it may name. A header written by the other end is bounded before it is turned into a number, and a delay above this is one nobody meant.
32- MAX_NAMED_DELAY_SECONDS =
(2**31) - 1
- WHOLE_SECONDS =
What a whole number of seconds reads as, which is the one form of that header this client honours.
/\A\d+\z/- EVENT_PATH =
Where an event is ingested, under the API URL.
"event"- EVENT_TYPES_PATH =
Where event types are read and created, under the API URL.
"event_types"
Instance Attribute Summary collapse
-
#api_url ⇒ String
readonly
The base API URL this client reaches.
-
#application_id ⇒ String
readonly
The application events are sent to.
-
#options ⇒ Options
readonly
The bounds one send is held to.
-
#transport ⇒ Transport
readonly
What this client issues its requests through, which is also what a generated operation group is built on.
Instance Method Summary collapse
-
#initialize(api_url, application_id, token, options = Options.new) ⇒ Client
constructor
A new instance of Client.
-
#send_event(event) ⇒ String
Sends an event, and answers the identifier it was sent under.
-
#upsert_event_types(event_types) ⇒ Array<String>
Creates the event types the application does not declare yet, and answers those.
Constructor Details
#initialize(api_url, application_id, token, options = Options.new) ⇒ Client
Returns a new instance of Client.
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/hook0/client.rb', line 440 def initialize(api_url, application_id, token, = Options.new) @api_url = api_url @application_id = application_id @options = @transport = Transport.new( api_url, token, timeout: .request_timeout, max_response_bytes: .max_response_bytes, max_response_headers: .max_response_headers, max_header_bytes: .max_header_bytes, max_head_bytes: .max_head_bytes, retry_policy: .retry_policy ) end |
Instance Attribute Details
#api_url ⇒ String (readonly)
Returns the base API URL this client reaches.
424 425 426 |
# File 'lib/hook0/client.rb', line 424 def api_url @api_url end |
#application_id ⇒ String (readonly)
Returns the application events are sent to.
427 428 429 |
# File 'lib/hook0/client.rb', line 427 def application_id @application_id end |
#options ⇒ Options (readonly)
Returns the bounds one send is held to.
430 431 432 |
# File 'lib/hook0/client.rb', line 430 def @options end |
#transport ⇒ Transport (readonly)
Returns what this client issues its requests through, which is also what a generated operation group is built on.
434 435 436 |
# File 'lib/hook0/client.rb', line 434 def transport @transport end |
Instance Method Details
#send_event(event) ⇒ String
Sends an event, and answers the identifier it was sent under.
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 |
# File 'lib/hook0/client.rb', line 461 def send_event(event) event_id = identifier_of(event) refuse_oversized(event, event_id) body = full_event(event, event_id) policy = @options.retry_policy delays = policy.delays(jitter_draws(policy.attempts - 1)) issued = 0 waited = 0.0 loop do issued += 1 outcome = attempt(body) return outcome.ingested unless outcome.ingested.nil? return event_id if outcome.already_ingested && issued > 1 raise ClientError.event_sending(event_id, outcome.detail) if outcome.already_ingested scheduled = outcome.retryable ? delays[issued - 1] : nil raise given_up(event_id, issued, waited, outcome.detail) if scheduled.nil? waiting = wait_for(outcome, scheduled, policy.max_total_delay_in_force - waited) sleep(waiting) waited += waiting end end |
#upsert_event_types(event_types) ⇒ Array<String>
Creates the event types the application does not declare yet, and answers those.
493 494 495 496 497 498 499 500 501 502 |
# File 'lib/hook0/client.rb', line 493 def upsert_event_types(event_types) wanted = event_types.map { |written| EventType.parse(written) } return [] if wanted.empty? declared = declared_event_types wanted.reject { |event_type| declared.include?(event_type.to_s) }.map do |event_type| create_event_type(event_type) event_type.to_s end end |