org.zeromq.timer.ZTicket Maven / Gradle / Ivy
Show all versions of jeromq Show documentation
package org.zeromq.timer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import zmq.util.Clock;
import zmq.util.Draft;
import zmq.util.Utils;
import zmq.util.function.Supplier;
/**
* Manages set of tickets.
*
* Ticket timers are very fast in the case where
* you use a lot of timers (thousands), and frequently remove and add them.
* The main use case is expiry timers for servers that handle many clients,
* and which reset the expiry timer for each message received from a client.
* Whereas normal timers perform poorly as the number of clients grows, the
* cost of ticket timers is constant, no matter the number of clients
*
* Tickets can be added with a given delay.
*
* When the delay of time expires after addition, handler method is executed with given arguments.
*
* Ticket is NOT repetitive and will be executed once unless canceled.
*
* This class is not thread-safe
*
* This is a DRAFT class, and may change without notice.
*/
@Draft
public final class ZTicket
{
/**
* Opaque representation of a ticket.
*/
public static final class Ticket implements Comparable
{
private final ZTicket parent;
private final TimerHandler handler;
private final Object[] args;
private long start;
private long delay;
private boolean alive = true;
private Ticket(ZTicket parent, long now, long delay, TimerHandler handler, Object... args)
{
assert (args != null);
this.parent = parent;
this.start = now;
this.delay = delay;
this.handler = handler;
this.args = args;
}
/**
* Resets the ticket.
*/
public void reset()
{
if (alive) {
parent.sort = true;
start = parent.now();
}
}
/**
* Cancels a ticket.
* @return true if cancelled, false if already cancelled.
*/
public boolean cancel()
{
if (alive) {
alive = false;
parent.sort = true;
return true;
}
return false;
}
/**
* Changes the delay of the ticket.
* @param delay the new delay of the ticket.
*/
public void setDelay(long delay)
{
if (alive) {
parent.sort = true;
this.delay = delay;
}
}
@Override
public int compareTo(Ticket other)
{
if (alive) {
if (other.alive) {
return Long.compare(start - other.start, other.delay - delay);
}
return -1;
}
return other.alive ? 1 : 0;
}
}
private final List tickets;
private final Supplier clock;
private boolean sort;
public ZTicket()
{
this(() -> TimeUnit.NANOSECONDS.toMillis(Clock.nowNS()));
}
ZTicket(Supplier clock)
{
this(clock, new ArrayList<>());
}
ZTicket(Supplier clock, List tickets)
{
this.clock = clock;
this.tickets = tickets;
}
private long now()
{
return clock.get();
}
private void insert(Ticket ticket)
{
sort = tickets.add(ticket);
}
/**
* Add ticket to the set.
* @param delay the expiration delay in milliseconds.
* @param handler the callback called at the expiration of the ticket.
* @param args the optional arguments for the handler.
* @return an opaque handle for further cancel and reset.
*/
public Ticket add(long delay, TimerHandler handler, Object... args)
{
if (handler == null) {
return null;
}
Utils.checkArgument(delay > 0, "Delay of a ticket has to be strictly greater than 0");
final Ticket ticket = new Ticket(this, now(), delay, handler, args);
insert(ticket);
return ticket;
}
/**
* Returns the time in millisecond until the next ticket.
* @return the time in millisecond until the next ticket.
*/
public long timeout()
{
if (tickets.isEmpty()) {
return -1;
}
sortIfNeeded();
// Tickets are sorted, so check first ticket
Ticket first = tickets.get(0);
long time = first.start - now() + first.delay;
if (time > 0) {
return time;
}
else {
return 0;
}
}
/**
* Execute the tickets.
* @return the number of tickets triggered.
*/
public int execute()
{
int executed = 0;
final long now = now();
sortIfNeeded();
Set cancelled = new HashSet<>();
for (Ticket ticket : this.tickets) {
if (now - ticket.start < ticket.delay) {
// tickets are ordered, not meeting the condition means the next ones do not as well
break;
}
if (!ticket.alive) {
// Dead ticket, let's continue
cancelled.add(ticket);
continue;
}
ticket.alive = false;
cancelled.add(ticket);
ticket.handler.time(ticket.args);
++executed;
}
for (int idx = tickets.size(); idx-- > 0; ) {
Ticket ticket = tickets.get(idx);
if (ticket.alive) {
break;
}
cancelled.add(ticket);
}
this.tickets.removeAll(cancelled);
cancelled.clear();
return executed;
}
private void sortIfNeeded()
{
if (sort) {
sort = false;
Collections.sort(tickets);
}
}
}