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.sse;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.time.*;
023import java.util.*;
024import java.util.concurrent.*;
025import java.util.concurrent.atomic.*;
026import java.util.function.*;
027
028import org.apache.juneau.marshall.sse.*;
029
030/**
031 * Subscriber queue for an {@link SseBroadcaster}.
032 */
033public class SseSubscription implements AutoCloseable, Iterable<SseEvent> {
034
035   private final String id;
036   private final LinkedBlockingDeque<SseEvent> queue;
037   private final AtomicBoolean closed;
038   private final Consumer<String> closeCallback;
039   // Serializes the drop-oldest retry loop in offer() across concurrent producers, so one producer's
040   // pollFirst()/offerLast() retry can't interleave with another's and evict a just-inserted event instead
041   // of the true oldest one.
042   private final Object offerLock = new Object();
043
044   /** How often {@link #poll(Duration)} re-checks {@link #isClosed()} while it waits. */
045   private static final long CLOSE_CHECK_NANOS = TimeUnit.MILLISECONDS.toNanos(50);
046
047   SseSubscription(String id, int queueSize, Consumer<String> closeCallback) {
048      if (isEmpty(id))
049         throw iaex("id cannot be null or empty.");
050      this.id = id;
051      this.queue = new LinkedBlockingDeque<>(queueSize);
052      this.closeCallback = reqnn("closeCallback", closeCallback);
053      closed = new AtomicBoolean(false);
054   }
055
056   /**
057    * The subscriber identifier.
058    *
059    * @return The subscriber identifier.
060    */
061   public String getId() {
062      return id;
063   }
064
065   /**
066    * Returns whether this subscription has been closed.
067    *
068    * @return {@code true} if this subscription has been closed.
069    */
070   public boolean isClosed() {
071      return closed.get();
072   }
073
074   /**
075    * Queues an event, dropping the oldest queued event when the queue is full.
076    *
077    * <p>
078    * Does nothing once the subscription is closed.  {@link SseBroadcaster#publish(SseEvent)} calls this for every
079    * subscriber; call it directly to address a single subscriber.
080    *
081    * @param event The event.  Must not be <jk>null</jk>.
082    * @return {@code true} if the oldest queued event was dropped to make room.
083    */
084   public boolean offer(SseEvent event) {
085      if (isClosed())
086         return false;
087      synchronized (offerLock) {
088         var dropped = false;
089         while (! queue.offerLast(event)) {
090            queue.pollFirst();
091            dropped = true;
092         }
093         return dropped;
094      }
095   }
096
097   /**
098    * Blocks until the next event is available.
099    *
100    * @return The next event.
101    * @throws InterruptedException If the wait was interrupted.
102    */
103   public SseEvent take() throws InterruptedException {
104      return queue.takeFirst();
105   }
106
107   /**
108    * Waits up to a timeout for the next event.
109    *
110    * <p>
111    * Unlike {@link #take()}, returns once the subscription is closed (within about 50 ms of the close), so a caller
112    * whose subscription was replaced or closed from another thread never hangs.  The 50 ms bound applies to a waiting
113    * {@code poll}; {@link #take()} is not woken by {@link #close()}.
114    *
115    * @param timeout How long to wait.  Must not be <jk>null</jk>.
116    * @return The next event, or <jk>null</jk> on timeout or once the subscription is closed.
117    * @throws InterruptedException If the wait was interrupted.
118    */
119   public SseEvent poll(Duration timeout) throws InterruptedException {
120      reqnn("timeout", timeout);
121      var deadline = System.nanoTime() + timeout.toNanos();
122      while (! isClosed()) {
123         var left = deadline - System.nanoTime();
124         if (left <= 0)
125            return null;
126         var event = queue.pollFirst(Math.min(left, CLOSE_CHECK_NANOS), TimeUnit.NANOSECONDS);
127         if (event != null)
128            return event;
129      }
130      return null;
131   }
132
133   @Override /* Iterable */
134   public Iterator<SseEvent> iterator() {
135      return new Iterator<>() {
136         @Override
137         public boolean hasNext() {
138            return ! isClosed();
139         }
140
141         @Override
142         public SseEvent next() {
143            try {
144               return take();
145            } catch (InterruptedException e) {
146               Thread.currentThread().interrupt();
147               throw new NoSuchElementException("Interrupted while waiting for SSE event.");
148            }
149         }
150      };
151   }
152
153   @Override /* AutoCloseable */
154   public void close() {
155      if (closed.compareAndSet(false, true)) {
156         queue.clear();
157         closeCallback.accept(id);
158      }
159   }
160}