001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.concurrent;
018
019
020import java.util.function.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022
023/**
024 * SPI for detecting reuse ("replay") of a one-time-use identifier.
025 *
026 * <p>
027 * A consumer wires a {@link ReplayCache} to narrow a token/identifier that would otherwise be usable any number
028 * of times within its lifetime down to single-use: the second and every subsequent submission of the same
029 * identifier is reported as a replay.  The SPI is deliberately generic &mdash; it deals only in a {@link String}
030 * identifier and a <c>long</c> absolute expiry, with no dependency on any particular protocol or transport.
031 *
032 * <p>
033 * <b>Atomic check-and-record (no TOCTOU).</b> {@link #checkAndRecord(String, long)} both checks whether an
034 * identifier has been seen before <i>and</i> records it as seen, as one indivisible operation.  A separate
035 * "seen" followed by a separate "record" would leave a race window in which two near-simultaneous submissions of
036 * the same identifier could both observe "not yet seen" and both proceed; implementations must guarantee that of
037 * any two concurrent calls with the same identifier, at most one returns <jk>true</jk>.
038 *
039 * <p>
040 * <b>Fail-mode is a consumer decision.</b> When the backing store cannot answer &mdash; for example a remote
041 * store is unreachable &mdash; an implementation may either throw or return an outcome.  The consumer decides how
042 * a thrown failure is resolved by passing a {@link FailMode} to
043 * {@link #checkAndRecord(String, long, FailMode, Consumer)}:
044 * <ul>
045 *    <li>{@link FailMode#FAIL_CLOSED} (the safe default) treats a store failure as a replay (reject).
046 *    <li>{@link FailMode#FAIL_OPEN} treats a store failure as first-seen (allow), degrading to the
047 *       multi-use-tolerant behavior a consumer would have with no {@link ReplayCache} wired at all.
048 * </ul>
049 * An implementation that wants to enforce its own outcome on store failure (rather than defer to the consumer's
050 * {@link FailMode}) can catch its own failures internally and return <jk>true</jk>/<jk>false</jk> from
051 * {@link #checkAndRecord(String, long)} instead of throwing.
052 *
053 * <p>
054 * <b>The built-in default is per-process only.</b> The zero-config built-in implementation
055 * ({@link InMemoryReplayCache}) is backed by process-local memory, so it only enforces single-use within one
056 * process.  Cross-node single-use requires a consumer-supplied {@link ReplayCache} backed by a store shared
057 * across every node.
058 *
059 * <h5 class='section'>See Also:</h5><ul>
060 *    <li class='jc'>{@link InMemoryReplayCache}
061 * </ul>
062 *
063 * @since 10.0.0
064 */
065public interface ReplayCache {
066
067   /**
068    * The policy applied by {@link ReplayCache#checkAndRecord(String, long, FailMode, Consumer)} when
069    * {@link ReplayCache#checkAndRecord(String, long)} throws (the backing store could not answer).
070    *
071    * @since 10.0.0
072    */
073   enum FailMode {
074
075      /** Treat a store failure as first-seen: allow the operation to proceed (multi-use-tolerant). */
076      FAIL_OPEN,
077
078      /** Treat a store failure as a replay: reject the operation (the safe default). */
079      FAIL_CLOSED
080   }
081
082   /**
083    * Atomically checks whether the specified identifier has been observed before and records it as seen if not.
084    *
085    * <p>
086    * <b>Implementations own their own timeout.</b> A call that blocks indefinitely (for example a remote store
087    * with no client-side deadline) blocks the caller with it.  An implementation backed by a network store must
088    * bound its own call so a slow/unreachable backend surfaces promptly as a throw (which the caller resolves
089    * per its chosen {@link FailMode}) or a return, never an unbounded stall.
090    *
091    * @param id The identifier to check.  Never <jk>null</jk>.
092    * @param expiresAtMs The identifier's own absolute expiry, in epoch milliseconds.  An implementation may use
093    *    this to self-evict a record once it can no longer matter.
094    * @return <jk>true</jk> if this is the first time the identifier has been observed (the caller should
095    *    proceed); <jk>false</jk> if it has been observed before (the caller should reject the submission as a
096    *    replay).
097    */
098   boolean checkAndRecord(String id, long expiresAtMs);
099
100   /**
101    * Invokes {@link #checkAndRecord(String, long)} applying the specified fail-mode policy on a thrown failure.
102    *
103    * <p>
104    * Equivalent to {@link #checkAndRecord(String, long, FailMode, Consumer) checkAndRecord(id, expiresAtMs,
105    * failMode, <jk>null</jk>)}.
106    *
107    * @param id The identifier to check.  Never <jk>null</jk>.
108    * @param expiresAtMs The identifier's own absolute expiry, in epoch milliseconds.
109    * @param failMode The policy to apply if {@link #checkAndRecord(String, long)} throws.  Must not be
110    *    <jk>null</jk>.
111    * @return <jk>true</jk> if the caller should proceed; <jk>false</jk> if the submission should be rejected.
112    */
113   default boolean checkAndRecord(String id, long expiresAtMs, FailMode failMode) {
114      return checkAndRecord(id, expiresAtMs, failMode, null);
115   }
116
117   /**
118    * Invokes {@link #checkAndRecord(String, long)} applying the specified fail-mode policy on a thrown failure.
119    *
120    * <p>
121    * If {@link #checkAndRecord(String, long)} throws, the exception is passed to <jv>onFailure</jv> (when
122    * non-<jk>null</jk>) and then resolved to <jk>true</jk> for {@link FailMode#FAIL_OPEN} or <jk>false</jk> for
123    * {@link FailMode#FAIL_CLOSED}.
124    *
125    * @param id The identifier to check.  Never <jk>null</jk>.
126    * @param expiresAtMs The identifier's own absolute expiry, in epoch milliseconds.
127    * @param failMode The policy to apply if {@link #checkAndRecord(String, long)} throws.  Must not be
128    *    <jk>null</jk>.
129    * @param onFailure A handler invoked with the thrown exception before the policy is applied.  Can be
130    *    <jk>null</jk>.
131    * @return <jk>true</jk> if the caller should proceed; <jk>false</jk> if the submission should be rejected.
132    */
133   default boolean checkAndRecord(String id, long expiresAtMs, FailMode failMode, Consumer<Exception> onFailure) {
134      reqnn("failMode", failMode);
135      try {
136         return checkAndRecord(id, expiresAtMs);
137      } catch (Exception e) {
138         if (onFailure != null)
139            onFailure.accept(e);
140         return failMode == FailMode.FAIL_OPEN;
141      }
142   }
143}