Class: Familia::MultiResult

Inherits:
Object
  • Object
show all
Defined in:
lib/familia/multi_result.rb

Overview

Represents the result of a Valkey/Redis transaction or pipeline operation.

This class encapsulates the outcome of a Database multi-command operation, providing access to both the command results and derived success status based on the presence of errors in the results.

A multi-command operation has three possible outcomes:

  1. Committed cleanly -- every queued command returned a value.
  2. Committed with errors -- the commands ran, but one or more returned an Exception object. Redis returns failed commands inside a transaction as exception objects rather than raising them, so the remaining commands still execute. #errors collects them.
  3. Aborted -- EXEC was discarded and no command ran at all. This is the documented outcome of a WATCH-guarded transaction whose watched key was modified by another client; redis-rb signals it by returning nil instead of an array. See #aborted?.

An aborted operation is a failure, but not an error in the sense of (2): no command ran, so there is nothing for #errors to report. #successful? is the method to test for the overall outcome; #errors? answers the narrower question of whether any individual command failed.

#results is always an Array -- an aborted operation reports an empty one rather than nil, so callers can index and iterate it unconditionally. Use #aborted? to tell an abort apart from an operation that committed zero commands.

Instances are read-only: #results and #errors are both frozen, since this object describes an operation that has already finished.

Examples:

Creating a MultiResult instance

result = Familia::MultiResult.new(["OK", "OK", 1])

Checking transaction success

if result.successful?
  puts "All commands completed without errors"
else
  puts "#{result.errors.size} command(s) failed"
end

Accessing individual command results

result.results.each_with_index do |value, index|
  puts "Command #{index + 1} returned: #{value}"
end

Inspecting errors

if result.errors?
  result.errors.each do |error|
    puts "Error: #{error.message}"
  end
end

Distinguishing an abort from a clean commit

if result.aborted?
  retry_the_transaction   # a watched key changed; nothing was applied
elsif result.errors?
  report(result.errors)   # the commands ran; some of them failed
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(results) ⇒ MultiResult

Creates a new MultiResult instance.

Parameters:

  • results (Array, nil)

    The raw results from Database commands. Exception objects in the array indicate command failures. nil means the transaction was discarded rather than executed, and is normalized to an empty array with #aborted? recording the distinction.



82
83
84
85
86
87
88
89
90
91
# File 'lib/familia/multi_result.rb', line 82

def initialize(results)
  @aborted = results.nil?
  # Frozen because this object describes an operation that already
  # finished -- its return values are history, not a working buffer. It
  # also keeps the memo in #errors coherent: that array is derived from
  # this one and cached, so a caller mutating this one afterwards would
  # leave the two disagreeing. Each instance freezes its own array rather
  # than sharing a constant, so no two results alias the same object.
  @results = (results || []).freeze
end

Instance Attribute Details

#resultsArray (readonly)

Returns The raw return values from the Database commands. Always a frozen Array; empty for an aborted operation.

Returns:

  • (Array)

    The raw return values from the Database commands. Always a frozen Array; empty for an aborted operation



71
72
73
# File 'lib/familia/multi_result.rb', line 71

def results
  @results
end

Instance Method Details

#aborted?Boolean

Whether the operation was discarded instead of executed.

redis-rb returns nil from #multi when EXEC is aborted, which happens when a WATCH-guarded transaction detects that a watched key changed under it. An aborted transaction applied none of its commands, so it reports no errors (#errors is empty) but is also not successful -- callers should retry rather than treat it as a no-op success.

This is the only way to distinguish an abort from an operation that committed zero commands, since both report an empty #results.

Returns:

  • (Boolean)

    true if EXEC was discarded before any command ran



105
106
107
# File 'lib/familia/multi_result.rb', line 105

def aborted?
  @aborted
end

#errorsArray<Exception>

Returns all Exception objects from the results array.

This method is memoized for performance when called multiple times on the same MultiResult instance. The returned array is frozen: it is derived state backing that memo, not a collection for callers to modify.

Returns:

  • (Array<Exception>)

    Frozen array of exceptions that occurred during execution; always empty for an aborted operation, which ran no commands and therefore produced no per-command failures



118
119
120
# File 'lib/familia/multi_result.rb', line 118

def errors
  @errors ||= results.grep(Exception).freeze
end

#errors?Boolean

Checks if any individual command failed.

An aborted operation reports false here -- it failed, but not because a command errored. Use #successful? to test the overall outcome.

Returns:

  • (Boolean)

    true if at least one command returned an Exception



128
129
130
# File 'lib/familia/multi_result.rb', line 128

def errors?
  !errors.empty?
end

#inspectString

Returns a summary of the outcome for logging and debugging.

Deliberately omits the command return values: they routinely carry field values read back out of the database, which have no business landing in a log line or an exception trace by default. Reach for #results when the values are what you actually want.

Returns:

  • (String)

    e.g. +#+



189
190
191
# File 'lib/familia/multi_result.rb', line 189

def inspect
  "#<#{self.class.name} #{outcome} size=#{size}>"
end

#sizeInteger

Returns the number of results in the multi-operation.

Returns:

  • (Integer)

    The number of individual command results returned; 0 for an aborted operation



163
164
165
# File 'lib/familia/multi_result.rb', line 163

def size
  results.size
end

#successful?Boolean Also known as: success?, areyouhappynow?

Checks if the operation ran and all commands completed successfully.

This is the primary method for determining if a multi-command operation completed without errors.

Returns:

  • (Boolean)

    true if the operation ran and no exceptions are in results, false otherwise (including an aborted operation)



139
140
141
# File 'lib/familia/multi_result.rb', line 139

def successful?
  !aborted? && errors.empty?
end

#to_hHash

Returns a hash representation of the result.

Includes :aborted so a failed result is self-describing -- otherwise a dumped abort is indistinguishable from a failure whose errors went missing.

The :results value is this object's own frozen array, not a copy, so a caller cannot reach through the hash to mutate internal state.

Returns:

  • (Hash)

    Hash with :success, :aborted, and :results keys



177
178
179
# File 'lib/familia/multi_result.rb', line 177

def to_h
  { success: successful?, aborted: aborted?, results: results }
end

#tupleArray Also known as: to_a

Returns a tuple representing the result of the operation.

Examples:

[true, ["OK", true, 1]]

Returns:

  • (Array)

    A tuple containing the success status and the raw results. The success status is a boolean indicating if all commands succeeded. The raw results is an array of return values from the Database commands.



154
155
156
# File 'lib/familia/multi_result.rb', line 154

def tuple
  [successful?, results]
end