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 — 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 — for example a remote 041 * store is unreachable — 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}