Module: Hooksmith::Idempotency

Defined in:
lib/hooksmith/idempotency.rb

Overview

Provides idempotency support for webhook processing.

Idempotency ensures that processing the same webhook multiple times (due to retries, network issues, etc.) produces the same result and doesn't cause duplicate side effects.

Examples:

Configure idempotency key extraction

Hooksmith.configure do |config|
  config.provider(:stripe) do |stripe|
    stripe.idempotency_key = ->(payload) { payload['id'] }
    stripe.register(:charge_succeeded, 'StripeChargeProcessor')
  end
end

Check if event was already processed

key = Hooksmith::Idempotency.extract_key(provider: :stripe, payload: payload)
if Hooksmith::Idempotency.already_processed?(provider: :stripe, key: key)
  return # Skip duplicate
end

Defined Under Namespace

Modules: Extractors

Class Method Summary collapse

Class Method Details

.already_processed?(provider:, key:) ⇒ Boolean

Checks if an event with the given idempotency key was already processed.

This requires the event store to be enabled and the model to respond to exists_with_idempotency_key? or have a find_by_idempotency_key method.

Parameters:

  • provider (Symbol, String)

    the provider name

  • key (String)

    the idempotency key

Returns:

  • (Boolean)

    true if already processed



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/hooksmith/idempotency.rb', line 51

def already_processed?(provider:, key:)
  return false if key.nil?

  config = Hooksmith.configuration.event_store_config
  return false unless config.enabled

  model_class = config.model_class
  return false unless model_class

  check_duplicate(model_class, provider.to_s, key)
rescue StandardError => e
  Hooksmith.logger.error("Failed to check idempotency for #{provider}: #{e.message}")
  false
end

.composite_key(*fields, separator: ':') ⇒ String

Generates a composite idempotency key from multiple fields.

Parameters:

  • fields (Array<String, Symbol, nil>)

    the fields to combine

  • separator (String) (defaults to: ':')

    the separator between fields

Returns:

  • (String)

    the composite key



71
72
73
# File 'lib/hooksmith/idempotency.rb', line 71

def composite_key(*fields, separator: ':')
  fields.compact.map(&:to_s).join(separator)
end

.extract_key(provider:, payload:) ⇒ String?

Extracts an idempotency key from a webhook payload.

Parameters:

  • provider (Symbol, String)

    the provider name

  • payload (Hash)

    the webhook payload

Returns:

  • (String, nil)

    the idempotency key or nil if not configured



32
33
34
35
36
37
38
39
40
41
# File 'lib/hooksmith/idempotency.rb', line 32

def extract_key(provider:, payload:)
  extractor = Hooksmith.configuration.idempotency_key_for(provider)
  return nil unless extractor

  key = extractor.call(payload)
  key&.to_s
rescue StandardError => e
  Hooksmith.logger.error("Failed to extract idempotency key for #{provider}: #{e.message}")
  nil
end