/*
* Copyright (c) 2000, 2022, Oracle and/or its affiliates.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl.
*/
package com.tangosol.net.topic;
import com.oracle.coherence.common.base.Exceptions;
import com.tangosol.io.AbstractEvolvable;
import com.tangosol.io.ExternalizableLite;
import com.tangosol.io.pof.PofReader;
import com.tangosol.io.pof.PofWriter;
import com.tangosol.io.pof.PortableObject;
import com.tangosol.net.FlowControl;
import com.tangosol.util.Binary;
import com.tangosol.util.ExternalizableHelper;
import com.tangosol.util.Filter;
import com.tangosol.util.function.Remote;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A {@link Subscriber} subscribes either directly to a {@link NamedTopic} or to a {@link NamedTopic#getSubscriberGroups()
* subscriber group} of a {@link NamedTopic}. Each value published to a {@link NamedTopic} is delivered to all of its
* {@link NamedTopic#getSubscriberGroups() subscriber groups} and direct (anonymous) {@link Subscriber}s.
*
* The factory methods {@link NamedTopic#createSubscriber(Subscriber.Option[])} or
* {@link com.tangosol.net.Session#createSubscriber(String, Option[])}
* allows one to specify one or more {@link Subscriber.Option}s to configure the {@link Subscriber}.
* The {@link Subscriber.Name#inGroup(String)} option specifies the subscriber group for the {@link Subscriber} to join.
* If this option is not specified, the {@link Subscriber} is a direct (anonymous) subscriber to the topic.
* All Subscriber options and defaults are summarized in a table in {@link Option}.
*
*
Channels
* Topics use the concept of channels to improve scalability. This is similar to how Coherence uses partition for caches
* but to avoid confusion the name channel was chosen. The default is the next prime above the square root of the
* partition count configured for the underlying cache service, which for the default partition count of 257 is 17
* channels.
*
* {@link Publisher Publishers} publish messages to a channel based on their ordering configuration. Subscribers then
* subscribe from channels by assigning channel ownership to subscribers. An anonymous subscriber has ownership of all
* channels. A subscriber that is part of a subscriber group has ownership of a sub-set of the available channels.
*
* Channel count is configurable, but ideally should not be set too high nor too low. For example setting the channel
* count to 1, would mean that all publishers contend to publish to a single channel, and that only one subscriber
* in a subscriber group will be able to receive messages. Setting the channel count too high (above say the number of
* publishers) may mean that some channels never receive any messages and are wasted. Finding the appropriate value is
* admittedly non-trivial, however when faced with maxing out throughput from a publisher's perspective this is a
* configuration that can be tweaked.
*
*
Subscriber Groups
* Subscribers can be part of a subscriber group. Within each subscriber group, each value is only
* {@link #receive() received} by one of the {@link Subscriber group members}, enabling distributed, parallel
* processing of the values delivered to the subscriber group. Thus, each subscriber group in effect behaves like a
* queue over the topic data.
*
* Subscribers in a group can be considered durable, if they are closed, or fail, then message processing will continue
* from the next element after the last committed position when subscriber reconnect.
*
* To maintain ordering or messages, only a single subscriber in a group polls for messages from a channel.
* Each subscriber in a group is allocated ownership of a sub-set of the channels in a topic. This means that
* the maximum number of subscribers in a group that could be doing any work would be the same as the topic's
* channel count. If there are more subscribers in a group that there are channels, then the additional subscribers
* will not be allocated ownership of any channels and hence will receive no messages.
* It may seem that increasing the channel count to a higher number would therefore allow more subscribers to work in
* parallel, but this is not necessarily the case.
*
* Channels are allocated to subscribers when they are created. As more subscribers in a group are created channel
* ownership is rebalanced, existing subscribers may lose ownership of channels that are allocated to new subscribers.
* As subscribers are {@link Subscriber#close() closed} (or die or are timed out), again channel ownership is rebalanced
* over the remaining subscribers.
*
* Subscribers in a group have a configurable timeout. If a subscriber does not call receive within the timeout it is
* considered dead and the channels it owns will be reallocated to other subscribers in the group. This is to stop
* channel starvation where no subscriber in a group is polling from a channel.
*
*
Positions
* Elements in a {@link NamedTopic} are published to a channel have a unique {@link Position} within that channel.
* A {@link Position} is an opaque representation of the underlying position as theoretically the implementation of
* the {@link Position} could change for different types of topic. Positions are used in various places in the API,
* for example, positions can be committed, and they can be used to move the subscriber to back or forwards within
* a channel. A {@link Position} is serializable, so they can be stored and recovered to later reset a subscriber
* to a desired position. Positions are {@link Comparable} so positions for elements can be used to determine whether
* how two elements related to each other within a channel.
*
* Committing
* Subscribers in a group are durable, so if they disconnect and reconnect, messages processing will restart from the
* correct position.
* A Subscriber can {@link Element#commit() commit an element} or {@link Subscriber#commit(int, Position) commit a
* position in a channel}. After a successful commit, then on reconnection the first message received from a channel
* will be the the next message after the committed position.
*
* When a position is committed, this will also commit any earlier positions in the channel. For example, if
* five elements are received and commit is called only on the last element, this effectively also commits the previous
* four elements.
*
* When topics are configured not to retain elements, received elements will be removed as they are committed by
* subscribers.
*
* If a subscriber in a subscriber group is timed-out (or dies) and its channel ownership is reallocated to other
* subscribers in the group, those subscribers will start to receive messages from the last committed position for
* of the channels from the failed subscriber.
*
* Commits may be performed synchronously (using {@link #commit(int, Position)}) or asynchronously (using
* {@link #commitAsync(int, Position)}). There is no facility for automatic commit of messages, all calls
* to commit must be done manually by application code.
*
*
Seeking
* A new subscriber will start to receive message from the head position of a channel (or the last committed position
* for a durable subscriber) and continue to receive messages in order until the tail is reached.
* It is possible to reposition a subscriber (backwards or forwards) using one of the {@link #seek(int, Instant)}
* methods. Seeking applies a new position to a specific channel. If an attempt is made to position the channel before
* the first available message (the channel's head) then the subscriber will be positioned at the head.
* Correspondingly, if an attempt is made to position a subscriber way beyond the last position in a channel
* (the channel's tail) then the subscriber will be positioned at the tail of the channel and will receive the
* next message published to that channel.
*
* It is also possible to seek to a timestamp. Published messages are given a timestamp, which is the
* {@link com.tangosol.net.Cluster#getTimeMillis() Coherence cluster time} on the storage enabled cluster member that
* accepted the published message. Using the {@link #seek(int, Instant)} method a subscriber can be repositioned so that
* the next message received from a channel is the first message with a timestamp greater than the timestamp
* used in the seek call. For example to position channel 0 so that the next message received is the first message after
* 20:30 on July 5th 2021:
*
* Instant timestamp = LocalDateTime.of(2021, Month.JULY, 5, 20, 30)
* .toInstant(ZoneOffset.UTC);
* subscriber.seek(0, timestamp);
*
*
* Seeking Forwards
* It is important to note that seeking forwards is skipping over messages, those skipped message will never
* be received once another commit is executed. Moving forwards does not alter the commit position, so when a
* subscriber has committed a position, then moves forwards and later fails, it will restart back at the commit.
*
* Seeking Backwards Over Previous Commits
* When topics are configured not to retain elements removal of elements occurs as their positions are committed, so
* they can never be received again.
* This means that in this case, it would not be possible to seek backwards further than the last commit, as those
* elements no longer exist in the topic.
*
* When topics are configured to retain elements then it is possible to seek backwards further than the last commit.
* This effectively rolls-back those commits and the previously committed messages after the new seek position will
* be re-received. If a subscriber fails after moving back in this fashion it will restart at the rolled-back position.
*
*
Receiving Elements
* Receiving elements from a topic is an asynchronous operation, calls to {@link #receive()} (or the batch
* version {@link #receive(int)}) return a {@link CompletableFuture}. If multiple calls are made to receive,
* the returned futures will complete in the correct order to maintain message ordering in a channel.
* To maintain ordering, the futures are completed by a single daemon thread. This means that code using
* any of the synchronous {@link CompletableFuture} handling patterns, such as
* {@link CompletableFuture#thenApply(Function)} or {@link CompletableFuture#thenAccept(Consumer)}, etc. will run on
* the same daemon thread, so application code in the handler methods must complete before the next receive future
* will be completed. Again, this is intentional, to maintain strict ordering of processing of received elements.
* If the {@link CompletableFuture} asynchronous handler methods are used such as,
* {@link CompletableFuture#thenApplyAsync(Function)} or {@link CompletableFuture#thenAcceptAsync(Consumer)}, etc.
* then the application code handling the received element will execute on another thread, and at this point there
* are no ordering guarantees.
*
* It is important that application code uses the correct handling of the returned futures to both maintain
* ordering (if that is important to the application) and also to have correct error handling, and not lose exceptions,
* which is easy to do in poorly written asynchronous future handler code.
*
*
Clean-Up
* Subscribers should ideally be closed when application code finishes with them. This will clean up server-side
* resources associated with a subscriber.
*
* It is also important (possibly more important) to {@link NamedTopic#destroySubscriberGroup(String) clean up
* subscriber groups} that are no longer required. Failure to delete a subscriber group will cause messages to be
* retained on the server that would otherwise have been removed, so consuming more server-side resources such as
* heap and disc.
*
* @param the type of the value returned by the subscriber
*
* @author jf/jk/mf 2015.06.03
* @since Coherence 14.1.1
*/
public interface Subscriber
extends AutoCloseable
{
/**
* Receive a value from the topic. If there is no value available then the future will complete according to
* the {@link CompleteOnEmpty} option used to create the {@link Subscriber}.
*
* Note: If the returned future is {@link CompletableFuture#cancel(boolean) cancelled} it is possible that a value
* may still be considered by the topic to have been received by this group, while the group would consider this
* a lost value. Subscriber implementations will make its best effort to prevent such loss, but it cannot be
* guaranteed and thus cancellation is not advisable.
*
* The {@link CompletableFuture futures} returned from calls to {@code receive} are completed sequentially.
* If the methods used to handle completion in application code block this will block completions of
* subsequent futures. This is to maintain ordering of consumption of completed futures.
* If the application code handles {@link CompletableFuture future} completion using the asynchronous methods
* of {@link CompletableFuture} (i.e. handling is handed off to another thread) this could cause out of order
* consumption as received values are consumed on different threads.
*
* @return a {@link CompletableFuture} which can be used to access the result of this completed operation
*
* @throws IllegalStateException if the {@link Subscriber} is closed
*/
public CompletableFuture> receive();
/**
* Receive a batch of {@link Element elements} from the topic.
*
* The {@code cMessage} parameter specifies the maximum number of elements to receive in the batch. The subscriber
* may return fewer elements than the {@code cMessage} parameter; this does not signify that the topic is empty.
*
* If there is no value available then the future will complete according to the {@link CompleteOnEmpty} option used
* to create the {@link Subscriber}.
*
* If the poll of the topic returns nothing (i.e. the topic was empty and {@link CompleteOnEmpty}) is true then the
* {@link Consumer} will not be called.
*
* The {@link CompletableFuture futures} returned from calls to {@code receive} are completed sequentially.
* If the methods used to handle completion in application code block this will block completions of
* subsequent futures. This is to maintain ordering of consumption of completed futures.
* If the application code handles {@link CompletableFuture future} completion using the asynchronous methods
* of {@link CompletableFuture} (i.e. handling is handed off to another thread) this could cause out of order
* consumption as received values are consumed on different threads.
*
* @param cBatch the maximum number of elements to receive in the batch
*
* @return a future which can be used to access the result of this completed operation
*
* @throws IllegalStateException if the {@link Subscriber} is closed
*/
public CompletableFuture>> receive(int cBatch);
/**
* Returns the current set of channels that this {@link Subscriber} owns.
*
* Subscribers that are part of a subscriber group own a sub-set of the available channels.
* A subscriber in a group should normally be assigned ownership of at least one channel. In the case where there
* are more subscribers in a group that the number of channels configured for a topic, then some
* subscribers will obviously own zero channels.
* Anonymous subscribers that are not part of a group are always owners all the available channels.
*
* @return the current set of channels that this {@link Subscriber} is the owner of, or an
* empty array if this subscriber has not been assigned ownership any channels
*/
public int[] getChannels();
/**
* Returns {@code true} if this subscriber is the owner of the specified channel.
*
* This method only really applies to subscribers that are part of a group that may own zero or more channels.
* This method will always return {@code true} for an anonymous subscriber that is not part of a group.
*
* As channel ownership may change as subscribers in a group are created and closed the result of this method
* is somewhat transient. To more accurately track channel ownership changes create a subscriber using the
* {@link ChannelOwnershipListeners} option
*
* @param nChannel the channel number
*
* @return {@code true} if this subscriber is currently the owner of the specified channel, otherwise
* returns {@code false} if the channel is not owned by this subscriber
*/
public default boolean isOwner(int nChannel)
{
int[] anChannel = getChannels();
for (int c : anChannel)
{
if (c == nChannel)
{
return true;
}
}
return false;
}
/**
* Returns the number of channels in the underlying {@link NamedTopic}.
*
* This could be different to the number of channels {@link #getChannels() owned} by this {@link Subscriber}.
*
* @return the number of channels in the underlying {@link NamedTopic}
*/
public int getChannelCount();
/**
* Return the {@link FlowControl} object governing this subscriber.
*
* @return the FlowControl object.
*/
public FlowControl getFlowControl();
/**
* Close the Subscriber.
*
* Closing a subscriber ensures that no new {@link #receive()} requests will be accepted and all pending
* receive requests will be completed or safely cancelled.
*
* For a direct topic {@code Subscriber.close()} enables the release of storage resources for
* unconsumed values.
*
* For a {@link Subscriber group member}, {@code close()} indicates that this member has left its corresponding
* {@link Name group}. One must actively manage a {@link NamedTopic#getSubscriberGroups() NamedTopic's logical
* subscriber groups} since their life span is independent of active {@link Subscriber} group membership.
* {@link NamedTopic#destroySubscriberGroup(String)} releases storage and stops accumulating topic values for a
* subscriber group.
*
* Calling {@code close()} on an already closed subscriber is a no-op.
*/
@Override
public void close();
/**
* Send a heartbeat to the server to keep this subscriber alive.
*
* Heartbeat messages are sent on calls to any of the {@link #receive()} methods.
* If a subscriber does not call receive within the configured timeout time, it will be considered dead
* and lose its channel ownership. This is to stop badly behaving subscribers from blocking messages
* from being processed, for example if a bug in application code causes a subscriber to deadlock or take
* an excessive amount of time to process messages.
*
* If application code knows that it will take longer to execute than the subscriber timeout, for example
* the message processing communicates with a third-party system that is in some kind of back-off loop, then
* the subscriber can send a heartbeat to keep itself alive.
*
* A subscriber that times-out is still active and is not closed. On the next call to a {@link #receive()} method
* the subscriber will reconnect and be reallocated channel ownerships. Although a timed-out subscriber is not
* closed it would have lost ownership of channels and not be able to commit any messages that it was processing
* when it timed out. This is because another subscriber in the group may have already been allocated ownership of
* the same channels, already processed, and committed that same message. Allowing commits of unowned channels
* is configurable, by default a subscriber can only commit positions for channels it owns.
*
* Heart-beating only applies to subscribers that are members of a subscriber group. An anonymous subscriber
* owns all channels and no other subscriber is sharing the workload, so anonymous subscriber will not be
* timed-out. For an anonymous subscriber the heartbeat operation is a no-op.
*/
public void heartbeat();
/**
* Determine whether this {@link Subscriber} is active.
*
* @return {@code true} if this {@link Subscriber} is active
*/
public boolean isActive();
/**
* Add an action to be executed when this {@link Subscriber} is closed.
*
* @param action the action to execute
*/
public void onClose(Runnable action);
/**
* Commit the specified channel and position.
*
* @param nChannel the channel to commit
* @param position the position within the channel to commit
*
* @return the result of the commit request
*
* @throws IllegalStateException if the {@link Subscriber} is closed
*/
public default CommitResult commit(int nChannel, Position position)
{
return commit(Collections.singletonMap(nChannel, position)).get(nChannel);
}
/**
* Asynchronously commit the specified channel and position.
*
* @param nChannel the channel to commit
* @param position the position within the channel to commit
*
* @return the result of the commit request
*
* @throws IllegalStateException if the {@link Subscriber} is closed
*/
public CompletableFuture commitAsync(int nChannel, Position position);
/**
* Commit the specified channels and positions.
*
* @param mapPositions a map of channels napped to the position to commit
*
* @return a map of results of the commit request for each channel
*
* @throws IllegalStateException if the {@link Subscriber} is closed
*/
public default Map commit(Map mapPositions)
{
try
{
return commitAsync(mapPositions).get();
}
catch (InterruptedException | ExecutionException e)
{
throw Exceptions.ensureRuntimeException(e);
}
}
/**
* Asynchronously commit the specified channels and positions.
*
* @param mapPositions a map of channels mapped to the positions to commit
*
* @return a map of results of the commit request for each channel
*
* @throws IllegalStateException if the {@link Subscriber} is closed
*/
public CompletableFuture