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.rest.server.mcp.v20260728;
018
019/**
020 * Binding-owned MCP {@code subscriptions/listen} (SEP-2575) configuration for a v2 {@link McpRevision}.
021 *
022 * <p>
023 * Mirrors {@link McpMrtrConfig}/{@link McpCacheConfig}'s exact placement precedent: {@code subscriptions/listen}
024 * is a {@code 2026-07-28}-only feature, so its configuration lives here, on the v2 adapter, never on the
025 * revision-neutral {@code org.apache.juneau.rest.server.mcp.McpServerConfig}.
026 *
027 * <p>
028 * <b>Mutable during setup, effectively immutable once published.</b> Builder-less mutable-setup bean,
029 * following the same contract as {@link McpMrtrConfig}: configure fully before handing to a binding, never
030 * mutate afterward.
031 *
032 * <p>
033 * Unlike the {@code org.apache.juneau.rest.server.mcp.McpSubscriptionBroker} singleton (which holds live
034 * per-connection state), this type is knobs-only — every field is a tunable numeric limit with no embedded
035 * broker reference. It is one of {@link McpOptions}'s nested configs (see {@link McpOptions#getSubscriptions()}),
036 * memoized once per binding along with the rest of {@link McpOptions}; the broker itself is derived and
037 * memoized separately, sized from this config's {@link #getQueueSize() queueSize} (see
038 * {@link McpOptions#resolveSubscriptionBroker()}).
039 */
040public class McpSubscriptionsConfig {
041
042   /** Default cap on concurrently open {@code subscriptions/listen} streams per broker. */
043   public static final int DEFAULT_MAX_CONCURRENT_SUBSCRIPTIONS = 256;
044
045   /** Default per-subscription drop-oldest queue bound; matches {@code SseBroadcaster.DEFAULT_QUEUE_SIZE}. */
046   public static final int DEFAULT_QUEUE_SIZE = 1024;
047
048   /** Default heartbeat/keepalive interval in milliseconds. */
049   public static final long DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000L;
050
051   /** Default idle timeout in milliseconds; {@code 0} disables idle-timeout reaping. */
052   public static final long DEFAULT_IDLE_TIMEOUT_MS = 0L;
053
054   private int maxConcurrentSubscriptions = DEFAULT_MAX_CONCURRENT_SUBSCRIPTIONS;
055   private int queueSize = DEFAULT_QUEUE_SIZE;
056   private long heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;
057   private long idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS;
058
059   /**
060    * The cap on concurrently open {@code subscriptions/listen} streams per broker.
061    *
062    * @return The cap. Always {@code > 0}.
063    */
064   public int getMaxConcurrentSubscriptions() {
065      return maxConcurrentSubscriptions;
066   }
067
068   /**
069    * Sets the cap on concurrently open {@code subscriptions/listen} streams per broker.
070    *
071    * @param value The new value. Must be {@code > 0}.
072    * @return This object.
073    * @throws IllegalArgumentException If {@code value} is not {@code > 0}.
074    */
075   public McpSubscriptionsConfig setMaxConcurrentSubscriptions(int value) {
076      if (value <= 0)
077         throw new IllegalArgumentException("maxConcurrentSubscriptions " + value + " must be > 0");
078      maxConcurrentSubscriptions = value;
079      return this;
080   }
081
082   /**
083    * The per-subscription drop-oldest queue bound.
084    *
085    * @return The bound. Always {@code > 0}.
086    */
087   public int getQueueSize() {
088      return queueSize;
089   }
090
091   /**
092    * Sets the per-subscription drop-oldest queue bound.
093    *
094    * @param value The new value. Must be {@code > 0}.
095    * @return This object.
096    * @throws IllegalArgumentException If {@code value} is not {@code > 0}.
097    */
098   public McpSubscriptionsConfig setQueueSize(int value) {
099      if (value <= 0)
100         throw new IllegalArgumentException("queueSize " + value + " must be > 0");
101      queueSize = value;
102      return this;
103   }
104
105   /**
106    * The heartbeat/keepalive interval in milliseconds.
107    *
108    * @return The interval. Always {@code > 0}.
109    */
110   public long getHeartbeatIntervalMs() {
111      return heartbeatIntervalMs;
112   }
113
114   /**
115    * Sets the heartbeat/keepalive interval.
116    *
117    * @param value The new value. Must be {@code > 0}.
118    * @return This object.
119    * @throws IllegalArgumentException If {@code value} is not {@code > 0}.
120    */
121   public McpSubscriptionsConfig setHeartbeatIntervalMs(long value) {
122      if (value <= 0)
123         throw new IllegalArgumentException("heartbeatIntervalMs " + value + " must be > 0");
124      heartbeatIntervalMs = value;
125      return this;
126   }
127
128   /**
129    * The idle timeout in milliseconds.
130    *
131    * @return The timeout. Always {@code >= 0} ({@code 0} means disabled).
132    */
133   public long getIdleTimeoutMs() {
134      return idleTimeoutMs;
135   }
136
137   /**
138    * Sets the idle timeout.
139    *
140    * <p>
141    * <b>Keep this greater than {@link #getHeartbeatIntervalMs() heartbeatIntervalMs}.</b> A heartbeat
142    * counts as activity, so as long as {@code heartbeatIntervalMs < idleTimeoutMs}, heartbeats alone keep
143    * an otherwise-quiet stream alive indefinitely. Configuring {@code idleTimeoutMs} at or below
144    * {@code heartbeatIntervalMs} defeats that: the watchdog can fire between two heartbeats and silently
145    * reap a perfectly healthy connection. This setter accepts any non-negative value without checking
146    * against the currently-configured heartbeat interval (the two may be set in either order), so this is
147    * a configuration contract, not one this class enforces for you.
148    *
149    * @param value The new value. Must be {@code >= 0} ({@code 0} disables idle-timeout reaping).
150    * @return This object.
151    * @throws IllegalArgumentException If {@code value} is negative.
152    */
153   public McpSubscriptionsConfig setIdleTimeoutMs(long value) {
154      if (value < 0)
155         throw new IllegalArgumentException("idleTimeoutMs " + value + " must be >= 0");
156      idleTimeoutMs = value;
157      return this;
158   }
159}