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}