com.digitalasset.canton.participant.protocol.conflictdetection
RequestTracker
Companion object RequestTracker
trait RequestTracker extends AutoCloseable with NamedLogging
The request tracker handles all the tasks around conflict detection that are difficult to parallelize. In detail, it tracks in-flight requests, performs activeness checks, detects conflicts between requests, keeps track of the sequencer time, checks for timeouts, and orchestrates the updates to the com.digitalasset.canton.participant.store.ActiveContractStore. It does not write to the com.digitalasset.canton.participant.protocol.RequestJournal, though. The request tracker lives entirely in memory on a single compute node.
The request tracker is a deterministic component that depends only on the ACS state and the sequencer messages. The order and timing in which the request tracker receives this information does not matter. (Even more broadly, the participant has no functional notion of “wall-clock” time; all time and timestamps come from sequencer messages.) These properties of determinism and sequencer time are both important parts of Canton and critical to the crash recovery model.
To keep track of the sequencer time, the request tracker must be notified
of every message that is received from the sequencer.
These notifications need not happen in order, but they must happen eventually. Every such message has
an associated monotonically increasing SequencerCounter. If one of the sequencer counters is skipped,
the request tracker may stall indefinitely. Every message has an associated timestamp. Time must
increase with every message in the order given by the sequencer counter.
This means that for sequencer counters sc1
and sc2
with sc1
< sc2
, the associated timestamps ts1
and ts2
must satisfy ts1
< ts2
.
Requests are identified by RequestCounters, which themselves must be a monotonically increasing sequence without gaps.
The request tracker uses these time signals to determine on which requests a verdict can be issued,
which requests have timed out, and which requests and contract states can be safely evicted
from the tracker's internal state. We say that the request tracker has observed a timestamp
if it has been signalled this timestamp or a later one. The request tracker can progress
to a timestamp ts
if it has observed all timestamps up to and including ts
and all commit sets with commit times up to ts
have been added with RequestTracker.addCommitSet.
If the request tracker can progress to a timestamp ts
, then it must eventually complete all futures
that RequestTracker.addRequest and RequestTracker.addCommitSet have returned
and that are associated with a timestamp equal or prior to ts
.
The futures are associated to the following timestamps:
- Activeness result: the activeness time of the request, typically the timestamp on the request
- Timeout result: the decision time of the request
- Finalization result: the commit time of the request
The three methods RequestTracker.addRequest, RequestTracker.addResult, and RequestTracker.addCommitSet must normally be called in the given sequence; the latter two need not be called if the request has timed out. A request is in flight from the corresponding call to RequestTracker.addRequest until the request tracker has progressed to its decision time (if the request times out) or its commit time.
These three methods are idempotent while the request is in flight. That is, if one of the methods is called twice with the same arguments, provided that the first one succeeded, then the second call has no effect, but its return value is equivalent to what the first call returned. If the method is called twice for the same request counter with different arguments, the subsequent behavior of the request tracker becomes unspecified. The same applies if the same sequencer counter is supplied several times with different timestamps.
A request tracker is typically initialized with a SequencerCounter and a com.digitalasset.canton.data.CantonTimestamp, causing it to be ready to handle requests starting from the given sequencer counter (inclusive) and timestamp (exclusive).
Conflict detection
The request tracker checks whether a request uses only contracts that are active at the activeness timestamp of the request. To that end, it determines the ActivenessResult of the request. A non-conflicting request should be approved in a response by the participant. A conflicting request should be rejected.
To describe conflict behavior, we introduce a few concepts:
A conflict detection time is a triple (com.digitalasset.canton.data.CantonTimestamp, Kind
, SequencerCounter).
There are three kinds, and they are ordered by
Finalization < Timeout < Activeness
. Conflict detection times are ordered lexicographically. In other words,
(ts1, kind1, sc1) < (ts2, kind2, sc2)
if and only if
ts1 < ts2
or
ts1 == ts2 && kind1 < kind2
or
ts1 == ts2 && kind1 == kind2 && sc1 < sc2.
A request with SequencerCounter sc
and com.digitalasset.canton.data.CantonTimestamp ts
induces two conflict detection times:
The sequencing time at (ts, Activeness, sc)
and the timeout time at
(decisionTime, Timeout, sc) where decisionTime
is the decision time of the request.
Without logical reordering, the sequencing time is also the request's activeness time.
(Technically, the activeness time must lie between the sequencing time and the decision time;
see RequestTracker.addRequest.)
A result with SequencerCounter sc
and com.digitalasset.canton.data.CantonTimestamp ts
also defines two conflict detection times:
The verdict time (ts, Result, sc)
and the finalization time
(commitTime, Finalize, sc)
where commitTime
is the commit time of the request.
A request is active at some conflict detection time t
if all of the following holds:
- Its sequencing time is equal or prior to
t
. - If no finalization time is known for the request, then
t
is equal or prior to the request's timeout time. - If a finalization time is known for the request, then
t
is equal or prior to the finalization time.
The finalization time of a request is known if the result has been successfully signalled to the request tracker using RequestTracker.addResult.
A request is finalized at some conflict detection time t
if its finalization time is known and equal or prior to t
.
A contract is active at time t
if a request with activeness time at most t
activates it, and
there is no finalized request at time t
that has deactivated it.
Activation happens through creation and transfer-in.
Deactivation happens through archival and transfer-out.
An active contract c
is locked at time t
if one of the following cases holds:
- There is an active request at time
t
that activatesc
. In that case, we say that the contract is in activation. - There is an active request at time
t
that deactivatesc
and the request does not activatec
.
A contract may be locked because of activation and deactivation at the same time if there are two active requests,
one activating the contract and another deactivating it.
For example, if one request r1
creates a contract with sequencing time t1
and finalization time t1'
and another request r2
with sequencing time t2
between t1
and t1'
archives it (despite that r2
's activeness check fails),
then the contract is created at time t1
and archived at t2
.
Activeness of contracts is checked at the request's activeness time. The ActivenessResult lists all contracts from the ActivenessSet of the request that are either locked or whose precondition fails. The activeness check succeeds if the activeness result is empty.
- Alphabetic
- By Inheritance
- RequestTracker
- NamedLogging
- AutoCloseable
- AnyRef
- Any
- Hide All
- Show All
- Public
- Protected
Abstract Value Members
- abstract def addCommitSet(requestCounter: RequestCounter, commitSet: Try[CommitSet])(implicit traceContext: TraceContext): Either[CommitSetError, EitherT[Future, NonEmptyChain[RequestTrackerStoreError], Unit]]
Informs the request tracker of the effect of a request that is to be committed.
Informs the request tracker of the effect of a request that is to be committed. The effect is described by a CommitSet, namely which contracts should be deactivated and which ones activated.
This method may be called only after an addResult call for the same request.
- requestCounter
The request counter of the request
- commitSet
The commit set to describe the committed effect of the request A scala.util.Failure$ indicates that result processing failed and no commit set can be provided.
- returns
A future to indicate whether the request was successfully finalized. When this future completes, the request's effect is persisted to the store.ActiveContractStore and the store.TransferStore. The future fails with an exception if the commit set tries to activate or deactivate a contract that was not locked during the activeness check. Otherwise, activeness irregularities are reported as scala.Left$.
- See also
ConflictDetector.finalizeRequest
- abstract def addRequest(requestCounter: RequestCounter, sequencerCounter: SequencerCounter, requestTimestamp: CantonTimestamp, activenessTime: CantonTimestamp, decisionTime: CantonTimestamp, activenessSet: ActivenessSet)(implicit traceContext: TraceContext): Either[RequestAlreadyExists, Future[RequestFutures]]
Adds the confirmation request to the request tracker.
Adds the confirmation request to the request tracker. It schedules the activeness check for the
activenessTime
and the timeout of the request for thedecisionTime
, whereby a result exactly at thedecisionTime
is considered to be in time. If a scala.Right$ is returned, it also signals the arrival of the message withsequencerCounter
and timestamprequestTimestamp
, like the RequestTracker!.tick operation.The activeness check request is expressed as an activeness set comprising all the contracts that must be active and those that may be created or transferred-in. This operation returns a pair of futures indicating the outcome of the activeness check and the timeout state of the request, described below.
If a request with
requestCounter
andsequencerCounter
is added, the request tracker expects that eventually every request prior torequestCounter
will be added, and that every sequencer counter prior tosequencerCounter
will be signaled.If the request has been added before with the same parameters, but it is not yet finalized nor has it timed out, the method ignores the second call and returns the same futures as in the first call.
- requestCounter
The request counter. It must correspond to the sequencer counter and timestamp in the request journal. Must not be
Long.MaxValue
.- sequencerCounter
The sequencer counter on the request. Must not be
Long.MaxValue
.- requestTimestamp
The timestamp on the request.
- activenessTime
The timestamp when the activeness check should happen. Must be at least
requestTimestamp
and less than thedecisionTime
.- decisionTime
The timeout for the request. Must be after the
activenessTime
.- activenessSet
The activeness set that determines the checks at the
activenessTime
.- returns
scala.Right$ if the request was added successfully or this is a duplicate call for the request. One future for the result of the activeness check and one for signalling the timeout state of a request. These futures complete only when their result is determined: The result of the activeness check when the request tracker has progressed to the
activenessTime
. The timeout state when the request tracker has progressed to thedecisionTime
or a transaction result was added for the request. scala.Left$ if a request with the samerequestCounter
is in flight with different parameters.
- Exceptions thrown
- If the
requestTimestamp
orsequencerCounter
isLong.MaxValue
. - If the
requestTimestamp
is earlier than to where the request tracking has already progressed - If the
activenessTimestamp
is not between therequestTimestamp
(inclusive) and thedecisionTime
(exclusive).
- If the
- abstract def addResult(requestCounter: RequestCounter, sequencerCounter: SequencerCounter, resultTimestamp: CantonTimestamp, commitTime: CantonTimestamp)(implicit traceContext: TraceContext): Either[ResultError, Unit]
Informs the request tracker that a result message has arrived for the given request.
Informs the request tracker that a result message has arrived for the given request. This marks the request as not having timed out. The actual effect of the result must be supplied via a subsequent call to RequestTracker!.addCommitSet. The request tracker may not progress beyond the
commitTime
until the commit set is provided.This method may be called only after a RequestTracker!.addRequest call for the same request.
- requestCounter
The request counter of the request
- sequencerCounter
The sequencer counter on the result message.
- resultTimestamp
The timestamp of the result message. Must be after the request's timestamp and at most the decision time.
- commitTime
The commit time, i.e., when the result takes effect Must be no earlier than the result timestamp.
- returns
scala.Right$ if the result was added successfully or this is a duplicate call for the request. The timeout future returned by the corresponding call to RequestTracker!.addRequest completes with RequestTracker$.NoTimeout. scala.Left$ if the result could not be added (see the RequestTracker$.ResultErrors for the possible cases). The timestamp is nevertheless considered as observed.
- Exceptions thrown
java.lang.IllegalArgumentException
if thecommitTime
is before theresultTimestamp
, or if the request is in flight and theresultTimestamp
is after its decision time. The timestamp is not observed in this case.
- abstract def awaitTimestamp(timestamp: CantonTimestamp): Option[Future[Unit]]
Returns a future that completes after the request has progressed to the given timestamp.
Returns a future that completes after the request has progressed to the given timestamp. If the request tracker has already progressed to the timestamp, scala.None is returned.
- abstract def close(): Unit
- Definition Classes
- AutoCloseable
- Annotations
- @throws(classOf[java.lang.Exception])
- abstract def getApproximateStates(coid: Seq[LfContractId])(implicit traceContext: TraceContext): Future[Map[LfContractId, ContractState]]
Returns a possibly outdated state of the contract.
- abstract def loggerFactory: NamedLoggerFactory
- Attributes
- protected
- Definition Classes
- NamedLogging
- abstract def tick(sequencerCounter: SequencerCounter, timestamp: CantonTimestamp)(implicit traceContext: TraceContext): Unit
Tells the request tracker that a message with the given sequencer counter and timestamp has been received.
Tells the request tracker that a message with the given sequencer counter and timestamp has been received.
If RequestTracker!.addRequest or RequestTracker!.addResult shall be called for the same message, RequestTracker!.tick may only be called after those methods as they may schedule tasks at the timestamp or later and task scheduling for a timestamp must be done before the time is observed.
A given sequencer counter may be signalled several times provided that all calls signal the same timestamp. Only the first such call is taken into account. Since RequestTracker!.addRequest and RequestTracker!.addResult implicitly signal the sequencer counter and timestamp to the request tracker (unless specified otherwise), it is safe to call this method after calling these methods.
The timestamps must increase with the sequencer counters.
- sequencerCounter
The sequencer counter of the message. Must not be
Long.MaxValue
.- timestamp
The timestamp on the message.
- Exceptions thrown
If one of the following conditions hold:
- If
sequencerCounter
isLong.MaxValue
. - If not all sequencer counters below
sequencerCounter
have been signalled and thetimestamp
does not increase with sequencer counters. - If all sequencer counters below
sequencerCounter
have been signalled and the timestamp is at most the timestamp of an earlier sequencer counter. - If
sequencerCounter
has been signalled before with a differenttimestamp
and not all sequencer counters belowsequencerCounter
have been signalled.
- If
Concrete Value Members
- final def !=(arg0: Any): Boolean
- Definition Classes
- AnyRef → Any
- final def ##: Int
- Definition Classes
- AnyRef → Any
- final def ==(arg0: Any): Boolean
- Definition Classes
- AnyRef → Any
- final def asInstanceOf[T0]: T0
- Definition Classes
- Any
- def clone(): AnyRef
- Attributes
- protected[lang]
- Definition Classes
- AnyRef
- Annotations
- @throws(classOf[java.lang.CloneNotSupportedException]) @native() @HotSpotIntrinsicCandidate()
- final def eq(arg0: AnyRef): Boolean
- Definition Classes
- AnyRef
- def equals(arg0: AnyRef): Boolean
- Definition Classes
- AnyRef → Any
- implicit def errorLoggingContext(implicit traceContext: TraceContext): ErrorLoggingContext
- Attributes
- protected
- Definition Classes
- NamedLogging
- final def getClass(): Class[_ <: AnyRef]
- Definition Classes
- AnyRef → Any
- Annotations
- @native() @HotSpotIntrinsicCandidate()
- def hashCode(): Int
- Definition Classes
- AnyRef → Any
- Annotations
- @native() @HotSpotIntrinsicCandidate()
- final def isInstanceOf[T0]: Boolean
- Definition Classes
- Any
- def logger: TracedLogger
- Attributes
- protected
- Definition Classes
- NamedLogging
- implicit def namedLoggingContext(implicit traceContext: TraceContext): NamedLoggingContext
- Attributes
- protected
- Definition Classes
- NamedLogging
- final def ne(arg0: AnyRef): Boolean
- Definition Classes
- AnyRef
- def noTracingLogger: Logger
- Attributes
- protected
- Definition Classes
- NamedLogging
- final def notify(): Unit
- Definition Classes
- AnyRef
- Annotations
- @native() @HotSpotIntrinsicCandidate()
- final def notifyAll(): Unit
- Definition Classes
- AnyRef
- Annotations
- @native() @HotSpotIntrinsicCandidate()
- final def synchronized[T0](arg0: => T0): T0
- Definition Classes
- AnyRef
- def toString(): String
- Definition Classes
- AnyRef → Any
- final def wait(arg0: Long, arg1: Int): Unit
- Definition Classes
- AnyRef
- Annotations
- @throws(classOf[java.lang.InterruptedException])
- final def wait(arg0: Long): Unit
- Definition Classes
- AnyRef
- Annotations
- @throws(classOf[java.lang.InterruptedException]) @native()
- final def wait(): Unit
- Definition Classes
- AnyRef
- Annotations
- @throws(classOf[java.lang.InterruptedException])