ferry

package ferry

Members list

Type members

Classlikes

object Ferry

Attributes

Companion
trait
Source
Ferry.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Ferry.type
trait Ferry[-I, +E, +A]

Coalesces individual calls into batched calls to an external system, and limits the load put on it.

Coalesces individual calls into batched calls to an external system, and limits the load put on it.

Each input submitted through apply is queued. Queued inputs are dispatched together in a single batch call when maxBatchSize inputs have accumulated, or at the latest maxBatchDelay after the oldest of them was submitted. At most maxInFlightBatches batch calls run at the same time, and at most maxQueueing inputs wait for a batch: beyond that, calls are rejected immediately rather than piling up.

Every caller receives its own share of the batch result, or the batch call's failure.

Attributes

Companion
object
Source
Ferry.scala
Supertypes
class Object
trait Matchable
class Any
object FerryConfig

Attributes

Companion
class
Source
FerryConfig.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class FerryConfig

Validated settings for a Ferry. Construct with FerryConfig.make.

Validated settings for a Ferry. Construct with FerryConfig.make.

Value parameters

maxBatchDelay

A partial batch is dispatched at the latest this long after its oldest input was submitted (or as soon as a batch slot frees up, if all slots were busy at that moment). Duration.Zero dispatches whatever is waiting immediately.

maxBatchSize

A batch is dispatched as soon as it holds this many inputs.

maxInFlightBatches

Maximum number of batch calls running concurrently.

maxQueueing

Maximum number of inputs waiting for a batch. Further inputs are rejected with FerryError.Rejected.

Attributes

Companion
object
Source
FerryConfig.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
enum FerryError[+E]

Why a call made through a Ferry did not produce a result.

Why a call made through a Ferry did not produce a result.

Attributes

Source
FerryError.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class FerryException[E](error: FerryError[E]) extends Exception

Attributes

Source
FerryError.scala
Supertypes
trait Product
trait Equals
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
final case class FoldWrite[-R, E, K, I, Id, S, O](id: I => Id, read: K => ZIO[R, E, S], step: (S, I) => (S, O), write: (K, S, S) => ZIO[R, E, Any], retry: Schedule[R, E, Any])

A batch call for inputs that must be applied in order to one key's state, in a store with conditional writes: read the state once, apply the batch's inputs to it in order, in memory, and write the result once, only if the store still holds the state that was read. A failed read or write, a conflict included, retries the whole round trip.

A batch call for inputs that must be applied in order to one key's state, in a store with conditional writes: read the state once, apply the batch's inputs to it in order, in memory, and write the result once, only if the store still holds the state that was read. A failed read or write, a conflict included, retries the whole round trip.

Each input gets its own outcome, computed when it is applied, from the state at that point: an input that changes how later ones apply, such as one closing a period, does so inside the fold. An input rejected on its own merits fails no other. A store that writes the result in two steps, a commit and a projection, uses ProjectedWrite. step sees the state as read, which includes inputs that an earlier attempt already applied (a write can land and its response be lost): it should answer such an input without applying it again, so that retrying is safe (see docs/decisions/0005).

Use it as the call and the extract of Ferry.perKey:

val write = FoldWrite(id = (e: Entry) => e.id, read = store.read, step = apply, write = store.writeIf, retry = policy)
Ferry.perKey(config, (e: Entry) => e.entity)(write.call)(write.outcome)

Value parameters

id

Identifies an input. Inputs of one batch with the same id are applied once, and share the first one's outcome.

read

Reads a key's state, including whatever makes the write conditional, such as a version.

retry

When to retry the round trip after read or write failed.

step

Applies one input to the state: the next state and the input's outcome. It must be pure; it runs again on retry.

write

Writes after for key if the store still holds before, and fails otherwise. Called only when the batch changed the state (after != before).

Attributes

Source
FoldWrite.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object InvalidConfig

Attributes

Companion
class
Source
FerryConfig.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class InvalidConfig(problems: NonEmptyChunk[String]) extends IllegalArgumentException

Attributes

Companion
object
Source
FerryConfig.scala
Supertypes
trait Product
trait Equals
class IllegalArgumentException
class RuntimeException
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all
object Lanes

Attributes

Source
Lanes.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
Lanes.type
object Order

Attributes

Companion
trait
Source
Order.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Order.type
trait Order[-I]

The order in which a Ferry.perKey sends a key's waiting inputs: ascending rank, ties in arrival order.

The order in which a Ferry.perKey sends a key's waiting inputs: ascending rank, ties in arrival order.

Inputs that wait together are sent in rank order, so each batch is in rank order and, under load, so is everything that arrived during the previous round trip. An input that arrives after inputs of a higher rank were already sent is not held back or rejected: it goes in the key's next batch, and the downstream, which holds the key's state, decides where it belongs. To give out-of-order inputs more time to meet, set a positive maxBatchDelay.

rank is evaluated once per input, when it is submitted. It must be pure, total and cheap.

Implement it to rank by anything an input carries, e.g. an effective date as date.toEpochDay, a timestamp as epoch milliseconds, or a sequence number.

Attributes

Companion
object
Source
Order.scala
Supertypes
class Object
trait Matchable
class Any
object PerKeyConfig

Attributes

Companion
class
Source
PerKeyConfig.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class PerKeyConfig

Validated settings for a Ferry.perKey. Construct with PerKeyConfig.make.

Validated settings for a Ferry.perKey. Construct with PerKeyConfig.make.

Value parameters

directIdleKeys

Whether, with a zero maxBatchDelay, an idle key's input is sent at once by its own caller (the default), or by a worker like any batch. Turn it off when the downstream needs a hard bound on concurrent calls: direct calls are not counted against maxInFlightBatches.

maxBatchDelay

How long a partial batch may wait for more inputs after its oldest input was submitted. The default, zero, sends an idle key's input at once, directly from its caller, and whatever is waiting as soon as the key's previous call completes: under load batches then grow by themselves, at no added latency. A positive delay batches every input, even an idle key's.

maxBatchSize

A key's batch is dispatched as soon as it holds this many inputs.

maxBatchWeight

A key's batch is dispatched as soon as its inputs weigh this much in total, and never carries more, except for a single input that is heavier on its own: it travels alone. Use it to stay under a per-request byte limit downstream.

maxInFlightBatches

Maximum number of batch calls running concurrently, across all keys. Each key has at most one. Direct calls for idle keys are not counted.

maxQueuedWeight

Maximum total weight of the inputs waiting, across all keys. Further inputs are rejected with FerryError.Rejected. With weights in bytes, this bounds the memory held by waiting inputs.

maxQueueing

Maximum number of inputs waiting, across all keys. Further inputs are rejected with FerryError.Rejected.

Attributes

Companion
object
Source
PerKeyConfig.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Source
ProjectedWrite.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class ProjectedWrite[-R, E, K, I, Id, S, O](fold: FoldWrite[R, E, K, I, Id, S, O], pending: S => Boolean, project: (K, S) => ZIO[R, E, Projection[Id, E]])

A batch call for a store that writes a key's record in two dependent conditional writes: commit the folded state (fold.write, fenced on the state read), then project it into its read-side view (project, fenced on the committed record being as committed). The record says whether its projection is pending (pending), so a projection that did not happen, because the process failed or died between the writes, is finished later by resume, from the store, by any process. Nothing is kept between calls: the marker lives in the store (see docs/decisions/0014).

A batch call for a store that writes a key's record in two dependent conditional writes: commit the folded state (fold.write, fenced on the state read), then project it into its read-side view (project, fenced on the committed record being as committed). The record says whether its projection is pending (pending), so a projection that did not happen, because the process failed or died between the writes, is finished later by resume, from the store, by any process. Nothing is kept between calls: the marker lives in the store (see docs/decisions/0014).

The view may be one object per record, written whole by project, or one row per entry, each its own write (see docs/decisions/0015). In either case project answers with what it left pending: nothing, or the entries whose row is not in the view, each with why. An input whose row is in the view when the call returns is ProjectedWrite.Outcome.Projected; any other is ProjectedWrite.Outcome.Committed, carrying why: it is durable, and the view may or may not include it yet. The call fails only when the commit is not known to have happened, and retrying it is then safe, because step answers inputs already applied; a failed projection is reported, not failed, because the inputs are durable (see docs/decisions/0006).

Use it as the call and the extract of Ferry.perKey, like FoldWrite:

val write = ProjectedWrite(FoldWrite(_.id, store.read, apply, store.commit, policy), pending, store.projectRows)
Ferry.perKey(config, (e: Entry) => e.entity)(write.call)(write.outcome)

Value parameters

fold

The commit phase: read reads the record, including its fence and its projection marker; step returns the state the commit stores, for which pending holds exactly when there is something to project; write is the commit. The commit must never move the stored marker backwards: either it leaves the marker as stored, or it is fenced on the whole record read, marker included. retry applies to the commit, and to resume after a first projection attempt failed; that first attempt's error is not passed to it.

pending

Whether a state, as read or as committed, has a projection to write. It must be pure.

project

Writes the view of state and clears its marker. For a view of one object: writes it only if the store still holds state, fence and marker alike, fails otherwise, and answers Projection.complete; see ProjectedWrite.wholeRecord. For a view of one row per entry: writes each missing row, each write idempotent (a row already present is present, not an error); clears the marker only when every row is present, and only if the store still holds state; answers the entries still pending, each with why. It derives rows and ids from state alone: resume has no inputs. A failure means no verdict: the call re-reads and retries it with fold.retry. A verdict with entries pending is final for the call; the store owns its per-row retries.

Attributes

Companion
object
Source
ProjectedWrite.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all