All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.paremus.dosgi.net.proxy.PushEventSourceConnector Maven / Gradle / Ivy

Go to download

An implementation of OSGi Remote Service Admin, enhanced to provide support for OSGi Promises and streaming using OSGi PushStreams.

The newest version!
/*-
 * #%L
 * com.paremus.dosgi.net
 * %%
 * Copyright (C) 2016 - 2019 Paremus Ltd
 * %%
 * Licensed under the Fair Source License, Version 0.9 (the "License");
 * 
 * See the NOTICE.txt file distributed with this work for additional 
 * information regarding copyright ownership. You may not use this file 
 * except in compliance with the License. For usage restrictions see the 
 * LICENSE.txt file distributed with this work
 * #L%
 */
package com.paremus.dosgi.net.proxy;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.ToLongFunction;

import org.osgi.framework.ServiceException;

import com.paremus.dosgi.net.client.BeginStreamingInvocation;
import com.paremus.dosgi.net.client.ClientBackPressure;
import com.paremus.dosgi.net.client.EndStreamingInvocation;
import com.paremus.dosgi.net.message.AbstractRSAMessage.CacheKey;
import com.paremus.dosgi.net.pushstream.PushStreamFactory.OnConnect;
import com.paremus.dosgi.net.serialize.Serializer;

import io.netty.channel.Channel;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;

public class PushEventSourceConnector implements OnConnect {
	
	private final ConcurrentMap connections = 
			new ConcurrentHashMap<>();

	private final Channel _channel;
	
	private final Serializer _serializer;

	private Timer _timer;
	
	public PushEventSourceConnector(Timer _timer, Channel _channel, Serializer _serializer) {
		this._timer = _timer;
		this._channel = _channel;
		this._serializer = _serializer;
	}
	
	@Override
	public void connect(CacheKey key, EventExecutor worker, Future closeFuture, 
			ToLongFunction pushData, Consumer pushClose) {
		// We use an immediate executor here as we swap to a different thread for real delivery
		
		PushEventSourceConnection connection = connections.computeIfAbsent(key, 
				k -> new PushEventSourceConnection(_timer, key, ImmediateEventExecutor.INSTANCE, _channel, _serializer));

		new PushEventSourceClient(worker, closeFuture, connection, pushData, pushClose);
	}

}

class PushEventSourceConnection {
	
	// Mostly there will only be one connected consumer
	private List activeClients = new ArrayList<>(1);
	
	private final Timer _timer;
	private final CacheKey key;
	private final EventExecutor _executor;
	private final Channel _channel;
	private final Serializer _serializer;
	
	private final ClientBackPressure backPressureTemplate;

	private Promise closeFuture;

	private boolean closed;

	private Timeout timeout;
	
	public PushEventSourceConnection(Timer _timer, CacheKey key, EventExecutor _executor,
			Channel _channel, Serializer _serializer) {
		this._timer = _timer;
		this.key = key;
		this._executor = _executor;
		this._channel = _channel;
		this._serializer = _serializer;
		this.backPressureTemplate = new ClientBackPressure(key.getId(), key.getCallId(), 0);
		this.closeFuture = _executor.newPromise().addListener(this::setTimeout);
		
		setTimeout(null);
	}

	private void setTimeout(Future f) {
		synchronized (activeClients) {
			if(!closed) {
				timeout = _timer.newTimeout(t -> {
						synchronized (activeClients) {
							if(activeClients.isEmpty()) {
								closed = true;
								closeFuture.trySuccess(null);
							}
						}
					}, 30, TimeUnit.SECONDS);
			}
		}
	}

	public void register(PushEventSourceClient pushEventSourceClient) {
		synchronized (activeClients) {
			if (closed) {
				throw new ServiceException("The PushEventSource has expired", ServiceException.REMOTE);
			}
			boolean add = activeClients.isEmpty();
			activeClients.add(pushEventSourceClient);
			if(add) {
				timeout.cancel();
				_channel.writeAndFlush(new BeginStreamingInvocation(key.getId(), key.getCallId(), 
						_serializer, _executor, this::incomingData, this::incomingTerminal, closeFuture)).addListener(f -> {
							if(!f.isSuccess()) {
								incomingTerminal(new ServiceException("Unable to open the data stream",
										ServiceException.REMOTE, f.cause()));
							}
						});
			}
		}
	}
	
	private void incomingData(Object o) {
		// Data only ever comes in on a single thread and is pushed onto client threads
		synchronized (activeClients) {
			if (!closed) {
				BackPressureToken token = new BackPressureToken(_channel, backPressureTemplate);
				for (PushEventSourceClient client : activeClients) {
					client.data(token, o);
				}
			}
		}
	}

	private void incomingTerminal(Exception e) {
		
		List toCloseClients;
		Promise toClosePromise;
		synchronized (activeClients) {
			toCloseClients = new ArrayList<>(activeClients);
			activeClients.clear();
			toClosePromise = closeFuture;
			closeFuture = _executor.newPromise().addListener(this::setTimeout);
		}
	
		for (PushEventSourceClient client : toCloseClients) {
			client.terminal(e);
		}
		toClosePromise.trySuccess(null);
	}

	public void unregister(PushEventSourceClient pushEventSourceClient) {
		Promise toClosePromise = null;
		synchronized (activeClients) {
			if(activeClients.remove(pushEventSourceClient) && activeClients.isEmpty()) {
				_channel.writeAndFlush(new EndStreamingInvocation(key.getId(), key.getCallId()));
				toClosePromise = closeFuture;
				closeFuture = _executor.newPromise().addListener(this::setTimeout);
			}
		}
		if(toClosePromise != null) {
			toClosePromise.trySuccess(null);
		}
	}
}

class PushEventSourceClient {

	private final AtomicBoolean closed = new AtomicBoolean();
	private final EventExecutor worker;
	private final ToLongFunction pushData;
	private final Consumer pushClose;

	public PushEventSourceClient(EventExecutor worker, Future closeFuture, PushEventSourceConnection connection, ToLongFunction pushData,
			Consumer pushClose) {
		this.worker = worker;
		this.pushData = pushData;
		this.pushClose = pushClose;
		if(!closeFuture.isDone()) {
			closeFuture.addListener(f -> {
					connection.unregister(this);
					terminal(null);
				});
			connection.register(this);
		}
	}

	public void data(BackPressureToken token, Object o) {
		if(!closed.get()) {
			try {
				worker.execute(() -> internalDataEvent(token, o));
			} catch (Exception e) {
				checkedTerminal(e);
			}
		}
	}
	
	private void internalDataEvent(BackPressureToken token, Object o) {
		if(!closed.get()) {
			try {
				long bp = pushData.applyAsLong(o);
				if(bp > 0) {
					token.applyBackPressure(bp);
				} else if (bp < 0) {
					checkedTerminal(null);
				}
			} catch (Exception e) {
				checkedTerminal(e);
			}
		}
	}

	private void checkedTerminal(Exception e) {
		if(!closed.getAndSet(true)) {
			pushClose.accept(e);
		}
	}

	public void terminal(Exception e) {
		if(!closed.get()) {
			try {
				worker.execute(() -> {
					try {
						checkedTerminal(e);
					} catch (Exception x) {
						// Nothing to do here
					}
				});
			} catch (Exception e2) {
				checkedTerminal(e);
			}
		}
	}
}

class BackPressureToken {
	private long backPressureFutureTime;
	
	private final Channel _channel;
	
	private final ClientBackPressure backPressureTemplate;
	
	public BackPressureToken(Channel _channel, ClientBackPressure backPressureTemplate) {
		this._channel = _channel;
		this.backPressureTemplate = backPressureTemplate;
	}

	public void applyBackPressure(long bp) {
		
		long suggestedFutureTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(bp);
		
		if(suggestedFutureTime == 0) {
			suggestedFutureTime = 1;
		}
		
		long toSend = 0;
		
		synchronized (this) {
			if(backPressureFutureTime == 0) {
				// This happens the first time that we apply backpressure
				backPressureFutureTime = suggestedFutureTime;
				toSend = bp;
			} else {
				long diff = suggestedFutureTime - backPressureFutureTime;
				if(diff > 0) {
					// We have more back pressure to send, either the diff, or the full back pressure
					backPressureFutureTime = suggestedFutureTime;
					toSend = Math.min(TimeUnit.NANOSECONDS.toMillis(diff), bp);
				}
			}
		}
		
		if(toSend > 0) {
			_channel.writeAndFlush(backPressureTemplate.fromTemplate(toSend));
		}
	}
}