Class: Hook0::Client

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(api_url, application_id, token, options = Options.new) ⇒ Client

Returns a new instance of Client.

Parameters:

  • api_url (String)

    base API URL of a Hook0 instance, such as https://app.hook0.com/api/v1

  • application_id (String)

    identifier of the Hook0 application events are sent to

  • token (String)

    an authentication token valid for that application

  • options (Options) (defaults to: Options.new)

    the bounds one send is held to



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 = Options.new)
  @api_url = api_url
  @application_id = application_id
  @options = options
  @transport = Transport.new(
    api_url,
    token,
    timeout: options.request_timeout,
    max_response_bytes: options.max_response_bytes,
    max_response_headers: options.max_response_headers,
    max_header_bytes: options.max_header_bytes,
    max_head_bytes: options.max_head_bytes,
    retry_policy: options.retry_policy
  )
end

Instance Attribute Details

#api_urlString (readonly)

Returns the base API URL this client reaches.

Returns:

  • (String)

    the base API URL this client reaches



424
425
426
# File 'lib/hook0/client.rb', line 424

def api_url
  @api_url
end

#application_idString (readonly)

Returns the application events are sent to.

Returns:

  • (String)

    the application events are sent to



427
428
429
# File 'lib/hook0/client.rb', line 427

def application_id
  @application_id
end

#optionsOptions (readonly)

Returns the bounds one send is held to.

Returns:

  • (Options)

    the bounds one send is held to



430
431
432
# File 'lib/hook0/client.rb', line 430

def options
  @options
end

#transportTransport (readonly)

Returns what this client issues its requests through, which is also what a generated operation group is built on.

Returns:

  • (Transport)

    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.

Parameters:

Returns:

  • (String)

Raises:



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.

Parameters:

  • event_types (Array<String>)

Returns:

  • (Array<String>)

Raises:



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