/* =========================================================================
FmqClient.java
Generated header for FmqClient protocol client
-------------------------------------------------------------------------
Copyright (c) 1991-2012 iMatix Corporation -- http://www.imatix.com
Copyright other contributors as noted in the AUTHORS file.
This file is part of FILEMQ, see http://filemq.org.
This 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.
This software is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTA-
BILITY 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 this program. If not, see http://www.gnu.org/licenses/.
=========================================================================
*/
package org.filemq;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Map;
import org.zeromq.ZMQ;
import org.zeromq.ZMQ.Socket;
import org.zeromq.ZMQ.Poller;
import org.zeromq.ZContext;
import org.zeromq.ZThread;
import org.zeromq.ZMsg;
import org.zeromq.ZFrame;
// ---------------------------------------------------------------------
// Structure of our front-end API class
public class FmqClient {
private final static int MAX_SERVERS = 256;
ZContext ctx; // CZMQ context
Socket pipe; // Pipe through to client
// The client runs as a background thread so that we can run multiple
// engines at once. The API talks to the client thread over an inproc
// pipe.
// --------------------------------------------------------------------------
// Create a new FmqClient and a new client instance
public FmqClient ()
{
ctx = new ZContext ();
pipe = ZThread.fork (ctx, new ClientThread ());
}
// --------------------------------------------------------------------------
// Destroy the FmqClient and stop the client
public void destroy ()
{
pipe.send ("STOP");
pipe.recvStr ();
ctx.destroy ();
}
// --------------------------------------------------------------------------
// Load client configuration data
public void configure (final String configFile)
{
pipe.sendMore ("CONFIG");
pipe.send (configFile);
}
// --------------------------------------------------------------------------
// Set one configuration key value
public void setoption (final String path, final String value)
{
pipe.sendMore ("SETOPTION");
pipe.sendMore (path);
pipe.send (value);
}
// --------------------------------------------------------------------------
// Open connection to server
public void connect (final String endpoint)
{
pipe.sendMore ("CONNECT");
pipe.send (endpoint);
}
// --------------------------------------------------------------------------
// Wait for message from API
public ZMsg recv ()
{
return ZMsg.recvMsg (pipe);
}
// --------------------------------------------------------------------------
// Return API pipe handle for polling
public Socket handle ()
{
return pipe;
}
// --------------------------------------------------------------------------
public void subscribe (final String path)
{
assert (path != null);
pipe.sendMore ("SUBSCRIBE");
pipe.send (String.format("%s", path));
}
// --------------------------------------------------------------------------
public void setInbox (final String path)
{
assert (path != null);
pipe.sendMore ("SET INBOX");
pipe.send (String.format("%s", path));
}
// --------------------------------------------------------------------------
public void setResync (long enabled)
{
pipe.sendMore ("SET RESYNC");
pipe.send (String.format("%d", enabled));
}
// ---------------------------------------------------------------------
// State machine constants
private enum State {
start_state (1),
requesting_access_state (2),
subscribing_state (3),
ready_state (4),
terminated_state (5);
@SuppressWarnings ("unused")
private final int state;
State (int state)
{
this.state = state;
}
};
private enum Event {
initialize_event (1),
srsly_event (2),
rtfm_event (3),
_other_event (4),
orly_event (5),
ohai_ok_event (6),
ok_event (7),
finished_event (8),
cheezburger_event (9),
hugz_event (10),
send_credit_event (11),
icanhaz_ok_event (12);
@SuppressWarnings ("unused")
private final int event;
Event (int event)
{
this.event = event;
}
};
// There's no point making these configurable
private static final int CREDIT_SLICE = 1000000;
private static final int CREDIT_MINIMUM = (CREDIT_SLICE * 4) + 1;
// Subscription in memory
private static class Sub {
private Client client; // Pointer to parent client
private String inbox; // Inbox location
private String path; // Path we subscribe to
private Sub (Client client, String inbox, String path)
{
this.client = client;
this.inbox = inbox;
this.path = path;
}
private void destroy ()
{
}
// Return new cache object for subscription path
private Map <String, String> cache ()
{
// Get directory cache for this path
FmqDir dir = FmqDir.newFmqDir (path.substring(1), inbox);
if (dir != null) {
Map <String, String> cache = dir.cache ();
dir.destroy ();
return cache;
}
return null;
}
}
// ---------------------------------------------------------------------
// Context for the client thread
private static class Client {
// Properties accessible to client actions
private boolean connected; // Are we connected to server?
private List <Sub> subs; // Subscriptions
private Sub sub; // Subscription we want to send
private int credit; // Current credit pending
private FmqFile file; // File we're writing to
private Iterator <Sub> subIterator;
// Properties you should NOT touch
private ZContext ctx; // Own CZMQ context
private Socket pipe; // Socket to back to caller
private final Server [] servers; // Server connections
private int nbrServers; // How many connections we have
private boolean dirty; // If true, rebuild pollset
private boolean stopped; // Has client stopped?
private FmqConfig config; // Configuration tree
private int heartbeat; // Heartbeat interval
private void config ()
{
// Get standard client configuration
heartbeat = Integer.parseInt (
config.resolve ("client/heartbeat", "1")) * 1000;
}
private Client (ZContext ctx, Socket pipe)
{
this.ctx = ctx;
this.pipe = pipe;
this.servers = new Server [MAX_SERVERS];
this.config = new FmqConfig ("root", null);
config ();
subs = new ArrayList <Sub> ();
connected = false;
}
private void destroy ()
{
if (config != null)
config.destroy ();
for (int serverNbr = 0; serverNbr < nbrServers; serverNbr++) {
Server server = servers [serverNbr];
server.destory ();
}
for (Sub sub: subs)
sub.destroy ();
}
// Apply configuration tree:
// * apply client configuration
// * print any echo items in top-level sections
// * apply sections that match methods
private void applyConfig ()
{
// Apply echo commands and class methods
FmqConfig section = config.child ();
while (section != null) {
FmqConfig entry = section.child ();
while (entry != null) {
if (entry.name ().equals ("echo"))
zclock_log (entry.value ());
entry = entry.next ();
}
if (section.name ().equals ("subscribe")) {
String path = section.resolve ("path", "?");
// Store subscription along with any previous ones
// Check we don't already have a subscription for this path
for (Sub sub: subs) {
if (path.equals (sub.path))
return;
}
// Subscription path must start with '/'
// We'll do better error handling later
assert (path.startsWith ("/"));
// New subscription, store it for later replay
String inbox = config.resolve ("client/inbox", ".inbox");
sub = new Sub (this, inbox, path);
subs.add (sub);
}
else
if (section.name ().equals ("set_inbox")) {
String path = section.resolve ("path", "?");
config.setPath ("client/inbox", path);
}
else
if (section.name ().equals ("set_resync")) {
long enabled = Long.parseLong (section.resolve ("enabled", ""));
// Request resynchronization from server
config.setPath ("client/resync", enabled > 0 ? "1" :"0");
}
section = section.next ();
}
config ();
}
// Custom actions for state machine
private void trySecurityMechanism (Server server)
{
String login = config.resolve ("security/plain/login", "guest");
String password = config.resolve ("security/plain/password", "");
ZFrame frame = FmqSasl.plainEncode (login, password);
server.request.setMechanism ("PLAIN");
server.request.setResponse (frame);
}
private void connectedToServer (Server server)
{
connected = true;
}
private void getFirstSubscription (Server server)
{
subIterator = subs.iterator ();
if (subIterator.hasNext ()) {
sub = subIterator.next ();
server.next_event = Event.ok_event;
} else
server.next_event = Event.finished_event;
}
private void getNextSubscription (Server server)
{
if (subIterator.hasNext ()) {
sub = subIterator.next ();
server.next_event = Event.ok_event;
} else
server.next_event = Event.finished_event;
}
private void formatIcanhazCommand (Server server)
{
server.request.setPath (sub.path);
// If client app wants full resync, send cache to server
if (Integer.parseInt (config.resolve ("client/resync", "0")) == 1) {
server.request.insertOptions ("RESYNC", "1");
server.request.setCache (sub.cache ());
}
}
private void refillCreditAsNeeded (Server server)
{
// If credit has fallen too low, send more credit
int credit_to_send = 0;
while (server.credit < CREDIT_MINIMUM) {
credit_to_send += CREDIT_SLICE;
server.credit += CREDIT_SLICE;
}
if (credit_to_send > 0) {
server.request.setCredit (credit_to_send);
server.next_event = Event.send_credit_event;
}
}
private void processThePatch (Server server)
{
String inbox = config.resolve ("client/inbox", ".inbox");
String filename = server.reply.filename ();
// Filenames from server must start with slash, which we skip
assert (filename.startsWith ("/"));
filename = filename.substring (1);
if (server.reply.operation () == FmqMsg.FMQ_MSG_FILE_CREATE) {
if (server.file == null) {
server.file = new FmqFile (inbox, filename);
if (!server.file.output ()) {
// File not writeable, skip patch
server.file.destroy ();
server.file = null;
return;
}
}
// Try to write, ignore errors in this version
ZFrame frame = server.reply.chunk ();
FmqChunk chunk = new FmqChunk (frame.getData (), frame.size ());
if (chunk.size () > 0) {
server.file.write (chunk, server.reply.offset ());
server.credit -= chunk.size ();
}
else {
// Zero-sized chunk means end of file, so report back to caller
pipe.sendMore ("DELIVER");
pipe.sendMore (filename);
pipe.send (String.format ("%s/%s", inbox, filename));
server.file.destroy ();
server.file = null;
}
chunk.destroy ();
}
else
if (server.reply.operation () == FmqMsg.FMQ_MSG_FILE_DELETE) {
zclock_log ("I: delete %s/%s", inbox, filename);
FmqFile file = new FmqFile (inbox, filename);
file.remove ();
file.destroy ();
file = null;
}
}
private void logAccessDenied (Server server)
{
System.out.println ("W: server denied us access, retrying...");
}
private void logInvalidMessage (Server server)
{
System.out.println ("E: server claims we sent an invalid message");
}
private void logProtocolError (Server server)
{
System.out.println ("E: protocol error");
}
private void controlMessage ()
{
ZMsg msg = ZMsg.recvMsg (pipe);
String method = msg.popString ();
if (method.equals ("SUBSCRIBE")) {
String path = msg.popString ();
// Store subscription along with any previous ones
// Check we don't already have a subscription for this path
for (Sub sub: subs) {
if (path.equals (sub.path))
return;
}
// Subscription path must start with '/'
// We'll do better error handling later
assert (path.startsWith ("/"));
// New subscription, store it for later replay
String inbox = config.resolve ("client/inbox", ".inbox");
sub = new Sub (this, inbox, path);
subs.add (sub);
}
else
if (method.equals ("SET INBOX")) {
String path = msg.popString ();
config.setPath ("client/inbox", path);
}
else
if (method.equals ("SET RESYNC")) {
long enabled = Long.parseLong (msg.popString ());
// Request resynchronization from server
config.setPath ("client/resync", enabled > 0 ? "1" :"0");
}
else
if (method.equals ("CONFIG")) {
String config_file = msg.popString ();
config.destroy ();
config = FmqConfig.load (config_file);
if (config != null)
applyConfig ();
else {
System.out.printf ("E: cannot load config file '%s'\n", config_file);
config = new FmqConfig ("root", null);
}
}
else
if (method.equals ("SETOPTION")) {
String path = msg.popString ();
String value = msg.popString ();
config.setPath (path, value);
config ();
}
else
if (method.equals ("STOP")) {
pipe.send ("OK");
stopped = true;
}
else
if (method.equals ("CONNECT")) {
String endpoint = msg.popString ();
if (nbrServers < MAX_SERVERS) {
Server server = new Server (ctx, endpoint);
servers [nbrServers++] = server;
dirty = true;
serverExecute (server, Event.initialize_event);
} else
System.out.printf ("E: too many server connections (max %d)\n", MAX_SERVERS);
}
msg.destroy ();
}
// Execute state machine as long as we have events
private void serverExecute (Server server, Event event)
{
server.next_event = event;
while (server.next_event != null) {
event = server.next_event;
server.next_event = null;
switch (server.state) {
case start_state:
if (event == Event.initialize_event) {
server.request.setId (FmqMsg.OHAI);
server.request.send (server.dealer);
server.request = new FmqMsg (0);
server.state = State.requesting_access_state;
}
else
if (event == Event.srsly_event) {
logAccessDenied (server);
server.state = State.terminated_state;
}
else
if (event == Event.rtfm_event) {
logInvalidMessage (server);
server.state = State.terminated_state;
}
else {
// Process all other events
logProtocolError (server);
server.state = State.terminated_state;
}
break;
case requesting_access_state:
if (event == Event.orly_event) {
trySecurityMechanism (server);
server.request.setId (FmqMsg.YARLY);
server.request.send (server.dealer);
server.request = new FmqMsg (0);
server.state = State.requesting_access_state;
}
else
if (event == Event.ohai_ok_event) {
connectedToServer (server);
getFirstSubscription (server);
server.state = State.subscribing_state;
}
else
if (event == Event.srsly_event) {
logAccessDenied (server);
server.state = State.terminated_state;
}
else
if (event == Event.rtfm_event) {
logInvalidMessage (server);
server.state = State.terminated_state;
}
else {
// Process all other events
}
break;
case subscribing_state:
if (event == Event.ok_event) {
formatIcanhazCommand (server);
server.request.setId (FmqMsg.ICANHAZ);
server.request.send (server.dealer);
server.request = new FmqMsg (0);
getNextSubscription (server);
server.state = State.subscribing_state;
}
else
if (event == Event.finished_event) {
refillCreditAsNeeded (server);
server.state = State.ready_state;
}
else
if (event == Event.srsly_event) {
logAccessDenied (server);
server.state = State.terminated_state;
}
else
if (event == Event.rtfm_event) {
logInvalidMessage (server);
server.state = State.terminated_state;
}
else {
// Process all other events
logProtocolError (server);
server.state = State.terminated_state;
}
break;
case ready_state:
if (event == Event.cheezburger_event) {
processThePatch (server);
refillCreditAsNeeded (server);
}
else
if (event == Event.hugz_event) {
server.request.setId (FmqMsg.HUGZ_OK);
server.request.send (server.dealer);
server.request = new FmqMsg (0);
}
else
if (event == Event.send_credit_event) {
server.request.setId (FmqMsg.NOM);
server.request.send (server.dealer);
server.request = new FmqMsg (0);
}
else
if (event == Event.icanhaz_ok_event) {
}
else
if (event == Event.srsly_event) {
logAccessDenied (server);
server.state = State.terminated_state;
}
else
if (event == Event.rtfm_event) {
logInvalidMessage (server);
server.state = State.terminated_state;
}
else {
// Process all other events
logProtocolError (server);
server.state = State.terminated_state;
}
break;
case terminated_state:
if (event == Event.srsly_event) {
logAccessDenied (server);
server.state = State.terminated_state;
}
else
if (event == Event.rtfm_event) {
logInvalidMessage (server);
server.state = State.terminated_state;
}
else {
// Process all other events
}
break;
}
}
}
private void serverMessage (Server server)
{
if (server.reply != null)
server.reply.destroy ();
server.reply = FmqMsg.recv (server.dealer);
if (server.reply == null)
return; // Interrupted; do nothing
// Any input from server counts as activity
server.expires_at = System.currentTimeMillis () + heartbeat * 2;
if (server.reply.id () == FmqMsg.SRSLY)
serverExecute (server, Event.srsly_event);
else
if (server.reply.id () == FmqMsg.RTFM)
serverExecute (server, Event.rtfm_event);
else
if (server.reply.id () == FmqMsg.ORLY)
serverExecute (server, Event.orly_event);
else
if (server.reply.id () == FmqMsg.OHAI_OK)
serverExecute (server, Event.ohai_ok_event);
else
if (server.reply.id () == FmqMsg.CHEEZBURGER)
serverExecute (server, Event.cheezburger_event);
else
if (server.reply.id () == FmqMsg.HUGZ)
serverExecute (server, Event.hugz_event);
else
if (server.reply.id () == FmqMsg.ICANHAZ_OK)
serverExecute (server, Event.icanhaz_ok_event);
}
}
private static class Server {
// Properties accessible to server actions
private Event next_event; // Next event
private int credit; // Current credit pending
private FmqFile file; // File we're writing to
// Properties you should NOT touch
private final ZContext ctx; // Own CZMQ context
private int index; // Index into client->server_array
private Socket dealer; // Socket to back to server
private long expires_at; // Connection expires at
private State state; // Current state
private Event event; // Current event
private final String endpoint; // server endpoint
private FmqMsg request; // Next message to send
private FmqMsg reply; // Last received reply
private Server (ZContext ctx, String endpoint)
{
this.ctx = ctx;
this.endpoint = endpoint;
dealer = ctx.createSocket (ZMQ.DEALER);
request = new FmqMsg (0);
dealer.connect (endpoint);
state = State.start_state;
}
private void destory ()
{
ctx.destroySocket (dealer);
request.destroy ();
if (reply != null)
reply.destroy ();
}
}
// Finally here's the client thread itself, which polls its two
// sockets and processes incoming messages
private static class ClientThread
implements ZThread.IAttachedRunnable {
@Override
public void run (Object [] args, ZContext ctx, Socket pipe)
{
Client self = new Client (ctx, pipe);
while (!self.stopped && !Thread.currentThread().isInterrupted ()) {
Poller items = ctx.getContext ().poller ();
items.register (self.pipe, Poller.POLLIN);
int serverNbr = 0;
// Rebuild pollset if we need to
if (self.dirty) {
for (serverNbr = 0; serverNbr < self.nbrServers; serverNbr++) {
Server server = self.servers [serverNbr];
items.register (server.dealer, Poller.POLLIN);
}
}
if (items.poll (self.heartbeat) == -1)
break; // Context has been shut down
// Process incoming messages; either of these can
// throw events into the state machine
if (items.pollin (0))
self.controlMessage ();
// Here, array of sockets to servers
for (serverNbr = 0; serverNbr < self.nbrServers; serverNbr++) {
if (items.pollin (serverNbr + 1)) {
Server server = self.servers [serverNbr];
self.serverMessage (server);
}
}
}
self.destroy ();
}
}
public static void zclock_log (String fmt, Object ... args)
{
System.out.println (String.format (fmt, args));
}
}