Class RequestMatchers


public class RequestMatchers extends MockServerMatcherNotifier
Author:
jamesdbloom
  • Constructor Details

  • Method Details

    • withCandidateIndexThreshold

      public RequestMatchers withCandidateIndexThreshold(int threshold)
      Sets the candidate-index engagement threshold on this instance without mutating any global/system state (so tests stay parallel-safe). Below this many expectations firstMatchingExpectation(org.mockserver.model.RequestDefinition) runs the untouched linear scan; at or above it the candidate index engages. Used by the matcher's own tests and the matching benchmark to exercise both paths deterministically; production code relies on the constructor-resolved value (default DEFAULT_CANDIDATE_INDEX_THRESHOLD, overridable via -Dmockserver.candidateIndexThreshold) and need not call this. Values below 2 are clamped to 2 (a 0/1-expectation store can never benefit).
    • setStateBackend

      public void setStateBackend(StateBackend stateBackend)
      Sets the state backend reference and wires the expectation KV store as the source of truth. Called by HttpState after construction. The node-local httpRequestMatchers CPQ becomes a derived cache; all mutations route through the backend first.

      When a backend is wired, the node-local CPQ's maxSize is raised to Integer.MAX_VALUE so that eviction is controlled exclusively by the backend (avoiding insertion-order divergence between two CPQs on update-in-place vs re-insert). reconcileEvictions() trims the node-local cache after each backend mutation.

      Threading contract (issue #2579): control-plane mutations are NOT externally serialized — PUT /mockserver/expectation adds run concurrently on the nioEventLoopThreadCount Netty worker threads (one per connection), and a clustered backend fires invalidation callbacks from its own threads. Correctness is guaranteed internally by two mechanisms, deliberately kept separate:

      1. The node-local structure mutation is serialized on this instance's monitor. add(org.mockserver.mock.Expectation, org.mockserver.mock.listeners.MockServerMatcherNotifier.Cause), update(org.mockserver.mock.Expectation[], org.mockserver.mock.listeners.MockServerMatcherNotifier.Cause), reset(org.mockserver.mock.listeners.MockServerMatcherNotifier.Cause), removeHttpRequestMatcher(org.mockserver.matchers.HttpRequestMatcher, java.lang.String), trimEvictedFromBackend() and reconcileClusteredScan() mutate the non-thread-safe httpRequestMatchers (CircularPriorityQueue) and expectationRequestDefinitions (CircularHashMap) only inside a synchronized(this) critical section. Each such section is short, purely in-memory, and — this is the load-bearing invariant — contains NO backend call, NO listener notification and NO blocking I/O.
      2. The eviction reconcile protects in-flight adds. An add registers its id in addsInFlight before mutating the node-local cache and deregisters it only after its backend put returns; the trim excludes in-flight ids from eviction (see trimEvictedFromBackend()).
      What is deliberately NOT guaranteed: no lock spans a backend call. In particular add/update release the monitor BEFORE expectationBackend.put(...), and trimEvictedFromBackend/ reconcileClusteredScan take their backend snapshot BEFORE acquiring the monitor. This is the specific rule the first #2579 fix (reverted commit 98ab5d8de) violated: it held this monitor across expectationBackend.put, which for a clustered (Infinispan) backend is a blocking distributed round-trip, while the node's own invalidation listener needed the same monitor to progress — deadlocking the two nodes. Listener notifications (notifyListeners(org.mockserver.mock.RequestMatchers, org.mockserver.mock.listeners.MockServerMatcherNotifier.Cause)) are fired AFTER the monitor is released so a listener's blocking work (the file-persistence write lock and disk I/O) never runs under it. The READ / matching path (firstMatchingExpectation(org.mockserver.model.RequestDefinition), firstMatchingEarlyExpectation(org.mockserver.model.HttpRequest), the retrieve* family and size()) is intentionally NOT synchronized — it relies on the CPQ's eventually-consistent toSortedList() snapshot and must not be throttled by the mutator monitor.
    • getStateBackend

      public StateBackend getStateBackend()
      Returns the state backend, or null if none has been set.
    • applyConfigurationCapacity

      public void applyConfigurationCapacity()
      Re-read maxExpectations from the Configuration and resize the expectation store in place, so a change made via PUT /mockserver/configuration actually takes effect instead of being accepted and ignored. A shrink evicts the eldest expectations immediately (firing the same eviction path as an overflow).

      When a state backend is wired (the default — HttpState always wires one) the backend is the eviction authority and the node-local queue is deliberately unbounded (setStateBackend(org.mockserver.state.StateBackend)), so the resize is applied to the BACKEND and the node-local view is then reconciled to drop anything the backend evicted. Without a backend the node-local queue carries the bound and is resized directly.

    • add

      public Expectation add(Expectation expectation, MockServerMatcherNotifier.Cause cause)
    • update

      public void update(Expectation[] expectations, MockServerMatcherNotifier.Cause cause)
    • size

      public int size()
    • reset

      public void reset(MockServerMatcherNotifier.Cause cause)
    • reset

      public void reset()
    • firstMatchingExpectation

      public Expectation firstMatchingExpectation(RequestDefinition requestDefinition)
    • firstMatchingEarlyExpectation

      public Expectation firstMatchingEarlyExpectation(HttpRequest headersOnlyRequest)
    • clear

      public void clear(RequestDefinition requestDefinition)
    • clearByNamespace

      public void clearByNamespace(String namespace, String logCorrelationId)
      Clears only the expectations belonging to the given namespace (tenant), leaving expectations in other namespaces (and global expectations) intact. This lets a tenant clean up after itself on a shared MockServer instance.

      When namespace is blank this is a no-op (use reset() or a request-matcher clear for a full clear) so that a blank namespace filter never accidentally clears global expectations.

      Parameters:
      namespace - the namespace whose expectations to clear
      logCorrelationId - correlation id for the resulting CLEARED log entry
    • clear

      public void clear(ExpectationId expectationId, String logCorrelationId)
    • reconcileFromBackend

      public void reconcileFromBackend()
      Reconciles the node-local HttpRequestMatcher cache against the backend KeyValueStore. This handles three cases:
      1. Eviction: cached matchers whose id is no longer in the backend are removed (mirrors maxExpectations eviction).
      2. Remote add: backend entries with no local matcher get a new compiled HttpRequestMatcher (enables cross-node visibility under clustering).
      3. Remote update: backend entries whose version is newer than the locally cached version get their matcher rebuilt.

      In single-node / no-backend mode this method is a no-op. When the backend is LOCAL (non-clustered), only eviction applies because all mutations originate locally and the CPQ is already in sync.

      Threading contract (issue #2579): this method is deliberately NOT synchronized at the method level, because it makes backend calls (expectationBackend.size()/entries()) and no lock may span a backend call (the rule the reverted commit 98ab5d8de broke, deadlocking a clustered backend). Instead trimEvictedFromBackend() and reconcileClusteredScan() take their backend snapshot OUTSIDE the monitor and then acquire synchronized(this) for ONLY the short, in-memory mutation of the node-local CPQ. Concurrent remote invalidation events therefore serialise on that inner monitor (so the CPQ is never corrupted) without ever blocking on the network while holding it.

      Concurrent matching (data-plane) note: this method applies incremental per-entry mutations (add/update/remove) to the CPQ — the same granularity as normal control-plane add/remove. The CPQ's toSortedList() provides an eventually-consistent sorted snapshot via ConcurrentSkipListSet + volatile sortedCache + filter(nonNull). A matching thread calling toSortedList() during a reconcile may see a snapshot that lags by one mutation, but will never see a torn/empty view. This matches the pre-existing control-plane / data-plane concurrency contract.

    • postProcess

      public Expectation postProcess(Expectation expectation)
    • retrieveRequestDefinitions

      public Stream<RequestDefinition> retrieveRequestDefinitions(List<ExpectationId> expectationIds)
    • retrieveActiveExpectations

      public List<Expectation> retrieveActiveExpectations(RequestDefinition requestDefinition)
    • peekFirstMatchingExpectation

      public Expectation peekFirstMatchingExpectation(RequestDefinition requestDefinition)
      Side-effect-free probe: returns the first active expectation whose matcher matches the given request, WITHOUT consuming the match. Specifically, this method avoids:
      • Times decrement (consumeMatch())
      • Scenario state transition
      • responseInProgress flag
      • Metrics increment

      Note on logging: the underlying HttpRequestMatcher.matches() call may still emit INFO-level EXPECTATION_MATCHED or EXPECTATION_NOT_MATCHED log entries as a side-effect of the match evaluation. This method does not suppress those match-diagnostic logs. It is the Times/scenario/responseInProgress/metrics side-effects that are avoided.

      Used by the gRPC bidi router to decide the routing path before committing to a handler. Callers that need to actually consume the match (decrement Times, transition scenarios, emit logs) must still call firstMatchingExpectation(RequestDefinition) separately on the committed path.

    • retrieveExpectationsMatchingRequest

      public List<Expectation> retrieveExpectationsMatchingRequest(RequestDefinition requestDefinition)
    • retrieveRequestMatchers

      public List<HttpRequestMatcher> retrieveRequestMatchers(RequestDefinition requestDefinition)
    • findClosestMatchDiff

      public Map<MatchDifference.Field,List<String>> findClosestMatchDiff(HttpRequest httpRequest)
    • findClosestMatchHint

      public RequestMatchers.ClosestMatchHint findClosestMatchHint(HttpRequest httpRequest)
      Find the expectation that came closest to matching the request (fewest field differences) together with its id and field diff. Cold-path only — invoked when a request matched nothing and a diagnostic is being produced. Returns null when there are no expectations (or none produced a usable diff).
    • isEmpty

      public boolean isEmpty()
    • getScenarioManager

      public ScenarioManager getScenarioManager()
    • notifyListeners

      protected void notifyListeners(RequestMatchers notifier, MockServerMatcherNotifier.Cause cause)
      Overrides:
      notifyListeners in class MockServerMatcherNotifier