/* * Copyright 2014-2015 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.cluck.rpc; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import ccre.cluck.CluckConstants; import ccre.cluck.CluckNode; import ccre.cluck.CluckSubscriber; import ccre.log.Logger; import ccre.time.Time; import ccre.util.UniqueIds; import ccre.verifier.FlowPhase; import ccre.verifier.SetupPhase; /** * A manager for the RPC subsystem in Cluck. * * @author skeggsc */ public final class RPCManager implements Serializable { private static final long serialVersionUID = -2530136013743162226L; private final CluckNode node; /** * The local end of the RPC binding. See the methods for publishing and * subscribing to RemoteProcedures. */ private final String localRPCBinding; private final HashMap<String, OutputStream> bindings = new HashMap<String, OutputStream>(); private final HashMap<String, Long> timeouts = new HashMap<String, Long>(); /** * Create a new RPCManager for the specified node. * * @param node The node to attach to. */ public RPCManager(CluckNode node) { this.node = node; localRPCBinding = UniqueIds.global.nextHexId("rpc-endpoint"); final HashMap<String, OutputStream> localBindings = this.bindings; final HashMap<String, Long> localTimeouts = this.timeouts; new CluckSubscriber(node) { @Override protected void receive(String source, byte[] data) { Logger.warning("Message to RPC endpoint!"); } @Override protected void receiveSideChannel(String dest, String source, byte[] data) { if (requireRMT(source, data, CluckConstants.RMT_INVOKE_REPLY)) { checkRPCTimeouts(); OutputStream stream; synchronized (RPCManager.this) { stream = localBindings.get(dest); } if (stream == null) { Logger.warning("No RPC binding for: " + dest); } else { try { stream.write(data, 1, data.length - 1); stream.close(); } catch (IOException ex) { Logger.warning("Exception in RPC response write!", ex); } synchronized (RPCManager.this) { localBindings.remove(dest); localTimeouts.remove(dest); } } } } @Override protected void receiveBroadcast(String source, byte[] data) { } }.attach(localRPCBinding); } /** * Publish a RemoteProcedure on the network. * * @param name The name for the RemoteProcedure. * @param proc The RemoteProcedure. */ @SetupPhase public void publish(final String name, final RemoteProcedure proc) { new CluckSubscriber(node) { @Override protected void receive(final String source, byte[] data) { if (requireRMT(source, data, CluckConstants.RMT_INVOKE)) { checkRPCTimeouts(); byte[] sdata = new byte[data.length - 1]; System.arraycopy(data, 1, sdata, 0, sdata.length); ByteArrayOutputStream baos = new ByteArrayOutputStream() { private boolean sent; @Override public void close() { if (sent) { throw new IllegalStateException("Already sent!"); } sent = true; node.transmit(source, name, toByteArray()); } }; baos.write(CluckConstants.RMT_INVOKE_REPLY); proc.invoke(sdata, baos); } } @Override protected void receiveBroadcast(String source, byte[] data) { defaultBroadcastHandle(source, data, CluckConstants.RMT_INVOKE); } }.attach(name); } /** * Check to see if any RPC calls have timed out and cancel them if they * have. This is only called when another RPC event occurs, so it may take a * while for the timeout to happen. */ @FlowPhase void checkRPCTimeouts() { // TODO: use scheduler rather than this hacky thing long now = Time.currentTimeMillis(); ArrayList<String> toRemove = new ArrayList<String>(); synchronized (this) { for (String key : timeouts.keySet()) { long value = timeouts.get(key); if (value < now) { toRemove.add(key); } } for (String rmt : toRemove) { timeouts.remove(rmt); try { bindings.remove(rmt).close(); } catch (IOException ex) { Logger.warning("Exception during timeout close!", ex); } } } } /** * Subscribe to a RemoteProcedure from the network at the specified path. * * @param path The path to subscribe to. * @param timeoutAfter How long should calls wait before they are canceled * due to timeout. * @return the RemoteProcedure. */ @SetupPhase public RemoteProcedure subscribe(final String path, final int timeoutAfter) { return new SubscribedProcedure(path, timeoutAfter); } @SetupPhase private void putNewInvokeBinding(String path, String localname, long timeoutAfter, OutputStream out, byte[] toSend) { synchronized (this) { timeouts.put(localname, Time.currentTimeMillis() + timeoutAfter); bindings.put(localname, out); } node.transmit(path, localRPCBinding + "/" + localname, toSend); } private class SubscribedProcedure implements RemoteProcedure, Serializable { private static final long serialVersionUID = 624324992717097477L; private final String path; private final int timeoutAfter; SubscribedProcedure(String path, int timeoutAfter) { this.path = path; this.timeoutAfter = timeoutAfter; } @Override public void invoke(byte[] in, OutputStream out) { checkRPCTimeouts(); String localname = UniqueIds.global.nextHexId(path); byte[] toSend = new byte[in.length + 1]; toSend[0] = CluckConstants.RMT_INVOKE; System.arraycopy(in, 0, toSend, 1, in.length); putNewInvokeBinding(path, localname, timeoutAfter, out, toSend); } } private Object writeReplace() { return new SerializedRPCManager(this.node); } private static class SerializedRPCManager implements Serializable { private static final long serialVersionUID = 8452028108928413549L; private final CluckNode node; public SerializedRPCManager(CluckNode node) { this.node = node; } private Object readResolve() { return node.getRPCManager(); } } }