Module: ActiveSanction::Error

Extended by:
T::Helpers, T::Sig
Included in:
ConfigurationError, InvalidArgument, MissingKey, SourceError, StorageError, Sync::Failed, UnsupportedError
Defined in:
lib/active_sanction/error.rb

Overview

The one rescue that covers this library.

begin
ActiveSanction.sync!
rescue ActiveSanction::Error => e
raise unless e.retryable?

RetryLater.enqueue(e.source_id)
end

Every error raised out of a public method answers to this, and carries what a caller needs to decide between the only three responses there are to a screening failure: retry this (retryable?), alert somebody (anything else that is not the caller's fault), and this is a bug in my call (ConfigurationError, InvalidArgument, QueryError). None of those decisions should be made by matching on a message string, so none of them has to be.

The hierarchy

Error                      the marker; rescue this
  ConfigurationError       this installation is set up wrong; never retry
  SourceError              something went wrong with one list
    FetchError             the bytes could not be obtained
    ParseError             the bytes could not be read
    IntegrityError         the bytes are not what they claim to be
  StorageError             the store could not answer
  UnsupportedError         this object cannot do that
  InvalidArgument          a public method was called wrongly
    QueryError             ...specifically, with an unusable query
  MissingKey               a field or column that does not exist

Why this is a module and not a class

Because two of its members have to be something else as well. A caller who passes threshold: 300 has made the mistake Ruby has had a class for since 1995, and rescue ArgumentError is what the code around this library already says; asking every host application to learn a private synonym for it would be this library exporting its own taxonomy into code that has no reason to care. So InvalidArgument is an ::ArgumentError and MissingKey is a ::KeyError -- and Ruby has one superclass to give. A module is what lets them be both, and rescue ActiveSanction::Error covers them anyway, because rescue matches with ===, which a module answers.

The trade is that ActiveSanction::Error cannot be raised or instantiated itself. That is not a loss: an error that says only "something in the sanctions library went wrong" is not one a caller could act on, and every member below names a response.

Stability

This hierarchy is public API. Within a major version an error will not move to a different parent, and an attribute will not be removed. New subclasses may be added under an existing parent -- that is what keeps rescue ActiveSanction::FetchError working when a new transport failure is given a name of its own -- so a case over error classes should carry an else.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#source_id ⇒ Symbol? (readonly)

Which list the failure belongs to, as the key its adapter declared, or nil for a failure that is not about one list -- a bad configuration, an unusable query, a store that will not open at all.

Always set on a SourceError by the time it leaves the source, even when the layer that raised it could not know: an HTTP client knows a URL, not which sanctions list is at the other end of it. See #in_source.

Returns:

  • (Symbol, nil)


79
80
81
# File 'lib/active_sanction/error.rb', line 79

def source_id
  @source_id
end

#status ⇒ Integer? (readonly)

The HTTP status behind the failure, where there was one. Nil for everything that failed before a server answered -- a timeout, a refused connection -- and for everything that is not a fetch.

Returns:

  • (Integer, nil)


85
86
87
# File 'lib/active_sanction/error.rb', line 85

def status
  @status
end

Instance Method Details

#in_source(key) ⇒ T.self_type

Stamps the list this failure belongs to onto an error raised by a layer that did not know it, and returns self so a rescue can re-raise in one line. Never overwrites a source already recorded -- the innermost layer that knew is the one that was right.

Parameters:

  • key (T.untyped)

Returns:

  • (T.self_type)


123
124
125
126
# File 'lib/active_sanction/error.rb', line 123

def in_source(key)
  @source_id = T.let(key&.to_sym, T.nilable(Symbol)) if @source_id.nil?
  self
end

#initialize(message = nil, source_id: nil, status: nil, retryable: nil) ⇒ void

retryable: overrides whatever the subclass would have decided, for the cases only the raising code knows about. Everything else is a subclass's own answer; see #retryable?.

Parameters:

  • message (T.untyped) (defaults to: nil)
  • source_id (T.untyped) (defaults to: nil)
  • status (T.untyped) (defaults to: nil)
  • retryable (Boolean, nil) (defaults to: nil)


93
94
95
96
97
98
# File 'lib/active_sanction/error.rb', line 93

def initialize(message = nil, source_id: nil, status: nil, retryable: nil)
  @source_id = T.let(source_id&.to_sym, T.nilable(Symbol))
  @status = T.let(status&.to_i, T.nilable(Integer))
  @retryable = T.let(retryable, T.nilable(T::Boolean))
  super(message)
end

#retryable? ⇒ Boolean

Whether running the same call again could plausibly succeed.

A first-class predicate rather than something a consumer reconstructs from the message, because backoff is the one decision a host application has to make in the request path and it should not be making it out of English. False is the default and the safe answer: a failure nobody has classified is one to look at rather than one to hammer.

Returns:

  • (Boolean)


108
# File 'lib/active_sanction/error.rb', line 108

def retryable? = retryable_or(false)

#to_h ⇒ Hash{Symbol => T.untyped}

The failure as data, for a log line or a job record that has to survive the process.

Returns:

  • (Hash{Symbol => T.untyped})


113
114
115
116
# File 'lib/active_sanction/error.rb', line 113

def to_h
  { error: self.class.name, message: message, source_id: source_id, status: status, retryable: retryable? }
    .compact
end