diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..6bb930a50 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2012-2015 Apcera Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/NATS.sln b/NATS.sln new file mode 100755 index 000000000..2cb65b0cb --- /dev/null +++ b/NATS.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2012 for Windows Desktop +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS", "NATS\NATS.csproj", "{68EE71D4-9532-470E-B5CA-ECAA79936B1F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATSUnitTests", "NATSUnitTests\NATSUnitTests.csproj", "{00DBFD4D-72F4-4250-884C-C1527C66A0C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Publish", "examples\Publish\Publish.csproj", "{A434BE66-EC4D-4FA4-89C7-9097A22319FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Subscribe", "examples\Subscribe\Subscribe.csproj", "{0D44FEE5-87D7-4DC5-956C-B03C3A5B286F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueueGroup", "examples\QueueGroup\QueueGroup.csproj", "{E8AECF76-0A83-4128-B2AA-27CBDA9CCA50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Requestor", "examples\Requestor\Requestor.csproj", "{6C2C965C-D1F9-4E5E-863C-EE5C51937866}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Replier", "examples\Replier\Replier.csproj", "{BEC6E07B-3DFC-493F-82E6-35A32D52ED2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {68EE71D4-9532-470E-B5CA-ECAA79936B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68EE71D4-9532-470E-B5CA-ECAA79936B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68EE71D4-9532-470E-B5CA-ECAA79936B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68EE71D4-9532-470E-B5CA-ECAA79936B1F}.Release|Any CPU.Build.0 = Release|Any CPU + {00DBFD4D-72F4-4250-884C-C1527C66A0C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00DBFD4D-72F4-4250-884C-C1527C66A0C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00DBFD4D-72F4-4250-884C-C1527C66A0C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00DBFD4D-72F4-4250-884C-C1527C66A0C2}.Release|Any CPU.Build.0 = Release|Any CPU + {A434BE66-EC4D-4FA4-89C7-9097A22319FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A434BE66-EC4D-4FA4-89C7-9097A22319FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A434BE66-EC4D-4FA4-89C7-9097A22319FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A434BE66-EC4D-4FA4-89C7-9097A22319FF}.Release|Any CPU.Build.0 = Release|Any CPU + {0D44FEE5-87D7-4DC5-956C-B03C3A5B286F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D44FEE5-87D7-4DC5-956C-B03C3A5B286F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D44FEE5-87D7-4DC5-956C-B03C3A5B286F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D44FEE5-87D7-4DC5-956C-B03C3A5B286F}.Release|Any CPU.Build.0 = Release|Any CPU + {E8AECF76-0A83-4128-B2AA-27CBDA9CCA50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8AECF76-0A83-4128-B2AA-27CBDA9CCA50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8AECF76-0A83-4128-B2AA-27CBDA9CCA50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8AECF76-0A83-4128-B2AA-27CBDA9CCA50}.Release|Any CPU.Build.0 = Release|Any CPU + {6C2C965C-D1F9-4E5E-863C-EE5C51937866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C2C965C-D1F9-4E5E-863C-EE5C51937866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C2C965C-D1F9-4E5E-863C-EE5C51937866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C2C965C-D1F9-4E5E-863C-EE5C51937866}.Release|Any CPU.Build.0 = Release|Any CPU + {BEC6E07B-3DFC-493F-82E6-35A32D52ED2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEC6E07B-3DFC-493F-82E6-35A32D52ED2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEC6E07B-3DFC-493F-82E6-35A32D52ED2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEC6E07B-3DFC-493F-82E6-35A32D52ED2D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/NATS/AsyncSub.cs b/NATS/AsyncSub.cs new file mode 100755 index 000000000..33fc76672 --- /dev/null +++ b/NATS/AsyncSub.cs @@ -0,0 +1,142 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +// disable XML comment warnings +#pragma warning disable 1591 + +namespace NATS.Client +{ + /// + /// An object of this class is an asynchronous subscription representing interest + /// in a subject. The subject can have wildcards (partial:*, full:>). + /// Messages will be delivered to the associated MsgHandler event delegates. + /// While nothing prevents event handlers from being added or + /// removed while processing messages, no messages will be received until + /// Start() has been called. This allows all event handlers to be added + /// before message processing begins. + /// + /// See MsgHandler. + public sealed class AsyncSubscription : Subscription, IAsyncSubscription, ISubscription + { + + public MsgHandler msgHandler = null; + private MsgHandlerEventArgs msgHandlerArgs = new MsgHandlerEventArgs(); + private Task msgFeeder = null; + + internal AsyncSubscription(Connection conn, string subject, string queue) + : base(conn, subject, queue) { } + + internal protected override bool processMsg(Msg msg) + { + Connection c; + MsgHandler handler; + long max; + + lock (mu) + { + c = this.conn; + handler = this.msgHandler; + max = this.max; + } + + // the message handler has not been setup yet, drop the + // message. + if (msgHandler == null) + return true; + + if (conn == null) + return false; + + long d = Interlocked.Increment(ref delivered); + if (max <= 0 || d <= max) + { + msgHandlerArgs.msg = msg; + try + { + msgHandler(this, msgHandlerArgs); + } + catch (Exception) { } + + if (d == max) + { + Unsubscribe(); + this.conn = null; + } + } + + return true; + } + + internal bool isStarted() + { + return (msgFeeder != null); + } + + internal void enableAsyncProcessing() + { + if (msgFeeder == null) + { + msgFeeder = new Task(() => { conn.deliverMsgs(mch); }); + msgFeeder.Start(); + } + } + + internal void disableAsyncProcessing() + { + if (msgFeeder != null) + { + mch.close(); + msgFeeder = null; + } + } + + /// + /// Adds or removes a message handler to this subscriber. + /// + /// See MsgHandler + public event MsgHandler MessageHandler + { + add + { + msgHandler += value; + } + remove + { + msgHandler -= value; + } + } + + /// + /// This completes the subsciption process notifying the server this subscriber + /// has interest. + /// + public void Start() + { + if (isStarted()) + return; + + if (conn == null) + throw new NATSBadSubscriptionException(); + + conn.sendSubscriptonMessage(this); + enableAsyncProcessing(); + } + + override public void Unsubscribe() + { + disableAsyncProcessing(); + base.Unsubscribe(); + } + + public override void AutoUnsubscribe(int max) + { + if (!isStarted()) + Start(); + + base.AutoUnsubscribe(max); + } + } +} \ No newline at end of file diff --git a/NATS/Channel.cs b/NATS/Channel.cs new file mode 100755 index 000000000..9fc9ba980 --- /dev/null +++ b/NATS/Channel.cs @@ -0,0 +1,104 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NATS.Client +{ + // Roll our own Channels - Concurrent Bag is more heavyweight + // than we need. + internal sealed class Channel + { + Queue q; + Object qLock = new Object(); + bool finished = false; + + internal Channel() + { + q = new Queue(1024); + } + + internal Channel(int initialCapacity) + { + q = new Queue(initialCapacity); + } + + internal T get(int timeout) + { + lock (qLock) + { + if (finished) + return default(T); + + if (q.Count > 0) + { + return q.Dequeue(); + } + else + { + if (timeout < 0) + { + Monitor.Wait(qLock); + } + else + { + if (Monitor.Wait(qLock, timeout) == false) + { + throw new NATSTimeoutException(); + } + } + + // we waited.. + if (finished) + return default(T); + + return q.Dequeue(); + } + } + + } // get + + internal void add(T item) + { + lock (qLock) + { + q.Enqueue(item); + + // if the queue count was previously zero, we were + // waiting, so signal. + if (q.Count <= 1) + { + Monitor.Pulse(qLock); + } + } + } + + internal void close() + { + lock (qLock) + { + finished = true; + Monitor.Pulse(qLock); + } + } + + internal int Count + { + get + { + lock (qLock) + { + return q.Count; + } + } + } + + } // class Channel + +} + diff --git a/NATS/Conn.cs b/NATS/Conn.cs new file mode 100755 index 000000000..9e151b5f7 --- /dev/null +++ b/NATS/Conn.cs @@ -0,0 +1,2137 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Sockets; + +// disable XM comment warnings +#pragma warning disable 1591 + +namespace NATS.Client +{ + /// + /// State of the connection. + /// + public enum ConnState + { + DISCONNECTED = 0, + CONNECTED, + CLOSED, + RECONNECTING, + CONNECTING + } + + internal class ServerInfo + { + internal string Id; + internal string Host; + internal int Port; + internal string Version; + internal bool AuthRequired; + internal bool SslRequired; + internal Int64 MaxPayload; + + Dictionary parameters = new Dictionary(); + + // A quick and dirty way to convert the server info string. + // .NET 4.5/4.6 natively supports JSON, but 4.0 does not, and we + // don't want to require users to to download a seperate json.NET + // tool for a minimal amount of parsing. + internal ServerInfo(string jsonString) + { + string[] kv_pairs = jsonString.Split(','); + foreach (string s in kv_pairs) + addKVPair(s); + + //parameters = kv_pairs.ToDictionary(v => v.Split(',')[0], v=>v.Split(',')[1]); + + Id = parameters["server_id"]; + Host = parameters["host"]; + Port = Convert.ToInt32(parameters["port"]); + Version = parameters["version"]; + + AuthRequired = "true".Equals(parameters["auth_required"]); + SslRequired = "true".Equals(parameters["ssl_required"]); + MaxPayload = Convert.ToInt64(parameters["max_payload"]); + } + + private void addKVPair(string kv_pair) + { + string key; + string value; + + kv_pair.Trim(); + string[] parts = kv_pair.Split(':'); + if (parts[0].StartsWith("{")) + key = parts[0].Substring(1); + else + key = parts[0]; + + if (parts[1].EndsWith("}")) + value = parts[1].Substring(0, parts[1].Length - 1); + else + value = parts[1]; + + key.Trim(); + value.Trim(); + + // trim off the quotes. + key = key.Substring(1, key.Length - 2); + + // bools and numbers may not have quotes. + if (value.StartsWith("\"")) + { + value = value.Substring(1, value.Length - 2); + } + + parameters.Add(key, value); + } + + } + + /// + /// Represents the connection to the server. + /// + public class Connection : IConnection, IDisposable + { + Statistics stats = new Statistics(); + + // NOTE: We aren't using Mutex here to support enterprises using + // .NET 4.0. + readonly object mu = new Object(); + + + private Random r = null; + + Options opts = new Options(); + + // returns the options used to create this connection. + public Options Opts + { + get { return opts; } + } + + List wg = new List(2); + + + private Uri url = null; + private LinkedList srvPool = new LinkedList(); + + // we have a buffered reader for writing, and reading. + // This is for both performance, and having to work around + // interlinked read/writes (supported by the underlying network + // stream, but not the BufferedStream). + private BufferedStream bw = null; + private BufferedStream br = null; + private MemoryStream pending = null; + + Object flusherLock = new Object(); + bool flusherKicked = false; + bool flusherDone = false; + + private ServerInfo info = null; + private Int64 ssid = 0; + + private ConcurrentDictionary subs = + new ConcurrentDictionary(); + + private Queue> pongs = new Queue>(); + + internal MsgArg msgArgs = new MsgArg(); + + internal ConnState status = ConnState.CLOSED; + + Exception lastEx; + + Parser ps = null; + System.Timers.Timer ptmr = null; + + int pout = 0; + + // Prepare static protocol messages to minimize encoding costs. + private byte[] pingProtoBytes = null; + private int pingProtoBytesLen; + private byte[] pongProtoBytes = null; + private int pongProtoBytesLen; + private byte[] _CRLF_AS_BYTES = Encoding.UTF8.GetBytes(IC._CRLF_); + + // Use a string builder to generate protocol messages. + StringBuilder publishSb = new StringBuilder(Defaults.scratchSize); + + TCPConnection conn = new TCPConnection(); + + + internal class Control + { + // for efficiency, assign these once in the contructor; + internal string op; + internal string args; + + static readonly internal char[] separator = { ' ' }; + + // ensure this object is always created with a string. + private Control() { } + + internal Control(string s) + { + string[] parts = s.Split(separator, 2); + + if (parts.Length == 1) + { + op = parts[0].Trim(); + args = IC._EMPTY_; + } + if (parts.Length == 2) + { + op = parts[0].Trim(); + args = parts[1].Trim(); + } + else + { + op = IC._EMPTY_; + args = IC._EMPTY_; + } + } + } + + /// + /// Convenience class representing the TCP connection to prevent + /// managing two variables throughout the NATs client code. + /// + private class TCPConnection + { + /// A note on the use of streams. .NET provides a BufferedStream + /// that can sit on top of an IO stream, in this case the network + /// stream. It increases performance by providing an additional + /// buffer. + /// + /// So, here's what we have: + /// Client code + /// ->BufferedStream (bw) + /// ->NetworkStream (srvStream) + /// ->TCPClient (srvClient); + /// + /// TODO: Test various scenarios for efficiency. Is a + /// BufferedReader directly over a network stream really + /// more efficient for NATS? + /// + Object mu = new Object(); + TcpClient client = null; + NetworkStream writeStream = null; + NetworkStream readStream = null; + + internal void open(Srv s, int timeoutMillis) + { + lock (mu) + { + + client = new TcpClient(s.url.Host, s.url.Port); +#if async_connect + client = new TcpClient(); + IAsyncResult r = client.BeginConnect(s.url.Host, s.url.Port, null, null); + + if (r.AsyncWaitHandle.WaitOne( + TimeSpan.FromMilliseconds(timeoutMillis)) == false) + { + client = null; + throw new NATSConnectionException("Timeout"); + } + client.EndConnect(r); +#endif + + client.NoDelay = false; + + client.ReceiveBufferSize = Defaults.defaultBufSize; + client.SendBufferSize = Defaults.defaultBufSize; + + writeStream = client.GetStream(); + readStream = new NetworkStream(client.Client); + } + } + + internal int ConnectTimeout + { + set + { + ConnectTimeout = value; + } + } + + internal int SendTimeout + { + set + { + client.SendTimeout = value; + } + } + + internal bool isSetup() + { + return (client != null); + } + + internal void teardown() + { + lock (mu) + { + TcpClient c = client; + NetworkStream ws = writeStream; + NetworkStream rs = readStream; + + client = null; + writeStream = null; + readStream = null; + + try + { + rs.Dispose(); + ws.Dispose(); + c.Close(); + } + catch (Exception) + { + // ignore + } + } + } + + internal BufferedStream getReadBufferedStream(int size) + { + return new BufferedStream(readStream, size); + } + + internal BufferedStream getWriteBufferedStream(int size) + { + return new BufferedStream(writeStream, size); + } + + internal bool Connected + { + get + { + if (client == null) + return false; + + return client.Connected; + } + } + + internal bool DataAvailable + { + get + { + if (readStream == null) + return false; + + return readStream.DataAvailable; + } + } + } + + private class ConnectInfo + { + bool verbose; + bool pedantic; + string user; + string pass; + bool ssl; + string name; + string lang = Defaults.LangString; + string version = Defaults.Version; + + internal ConnectInfo(bool verbose, bool pedantic, string user, string pass, + bool secure, string name) + { + this.verbose = verbose; + this.pedantic = pedantic; + this.user = user; + this.pass = pass; + this.ssl = secure; + this.name = name; + } + + /// + /// .NET 4 does not natively support JSON parsing. When moving to + /// support only .NET 4.5 and above use the JSON support provided + /// by Microsoft. (System.json) + /// + /// JSON string repesentation of the current object. + internal string ToJson() + { + StringBuilder sb = new StringBuilder(); + + sb.Append("{"); + + sb.AppendFormat("\"verbose\":{0},\"pedantic\":{1},", + verbose ? "true" : "false", + pedantic ? "true" : "false"); + if (user != null) + { + sb.AppendFormat("\"user\":\"{0}\",", user); + if (pass != null) + sb.AppendFormat("\"pass\":\"{0}\",", pass); + } + + sb.AppendFormat( + "\"ssl_required\":{0},\"name\":\"{1}\",\"lang\":\"{2}\",\"version\":\"{3}\"", + ssl ? "true" : "false", name, lang, version); + + sb.Append("}"); + + return sb.ToString(); + } + } + + // Ensure we cannot instanciate a connection this way. + private Connection() { } + + internal Connection(Options opts) + { + this.opts = opts; + this.pongs = createPongs(); + this.ps = new Parser(this); + this.pingProtoBytes = System.Text.Encoding.UTF8.GetBytes(IC.pingProto); + this.pingProtoBytesLen = pingProtoBytes.Length; + this.pongProtoBytes = System.Text.Encoding.UTF8.GetBytes(IC.pongProto); + this.pongProtoBytesLen = pongProtoBytes.Length; + } + + + /// Return bool indicating if we have more servers to try to establish + /// a connection. + private bool isServerAvailable() + { + return (srvPool.Count > 0); + } + + // Return the currently selected server + private Srv currentServer + { + get + { + if (!isServerAvailable()) + return null; + + foreach (Srv s in srvPool) + { + if (s.url.OriginalString.Equals(this.url.OriginalString)) + return s; + } + + return null; + } + } + + // Pop the current server and put onto the end of the list. Select head of list as long + // as number of reconnect attempts under MaxReconnect. + private Srv selectNextServer() + { + Srv s = this.currentServer; + if (s == null) + throw new NATSNoServersException("No servers are configured."); + + int num = srvPool.Count; + int maxReconnect = opts.MaxReconnect; + + // remove the current server. + srvPool.Remove(s); + + if (maxReconnect > 0 && s.reconnects < maxReconnect) + { + // if we haven't surpassed max reconnects, add it + // to try again. + srvPool.AddLast(s); + } + + if (srvPool.Count <= 0) + { + this.url = null; + return null; + } + + Srv first = srvPool.First(); + this.url = first.url; + + return first; + } + + // Will assign the correct server to the Conn.Url + private void pickServer() + { + this.url = null; + + if (!isServerAvailable()) + throw new NATSNoServersException("Unable to choose server; no servers available."); + + this.url = srvPool.First().url; + } + + private List randomizeList(string[] serverArray) + { + List randList = new List(); + List origList = new List(serverArray); + + Random r = new Random(); + + while (origList.Count > 0) + { + int index = r.Next(0, origList.Count); + randList.Add(origList[index]); + origList.RemoveAt(index); + } + + return randList; + } + + // Create the server pool using the options given. + // We will place a Url option first, followed by any + // Server Options. We will randomize the server pool unlesss + // the NoRandomize flag is set. + private void setupServerPool() + { + List servers; + + if (!string.IsNullOrWhiteSpace(Opts.Url)) + srvPool.AddLast(new Srv(Opts.Url)); + + if (Opts.Servers != null) + { + if (Opts.NoRandomize) + servers = new List(Opts.Servers); + else + servers = randomizeList(Opts.Servers); + + foreach (string s in servers) + srvPool.AddLast(new Srv(s)); + } + + // Place default URL if pool is empty. + if (srvPool.Count == 0) + srvPool.AddLast(new Srv(Defaults.Url)); + + pickServer(); + } + + // createConn will connect to the server and wrap the appropriate + // bufio structures. It will do the right thing when an existing + // connection is in place. + private bool createConn() + { + currentServer.updateLastAttempt(); + + try + { + conn.open(currentServer, opts.Timeout); + + if (pending != null && bw != null) + { + // flush to the pending buffer; + bw.Flush(); + } + + bw = conn.getWriteBufferedStream(Defaults.defaultBufSize * 6); + br = conn.getReadBufferedStream(Defaults.defaultBufSize * 6); + } + catch (Exception) + { + return false; + } + + return true; + } + + // makeSecureConn will wrap an existing Conn using TLS + private void makeTLSConn() + { + // TODO: Notes... SSL for beta? Encapsulate/overide writes to work with SSL and + // standard streams. Buffered writer with an SSL stream? + } + + // waitForExits will wait for all socket watcher Go routines to + // be shutdown before proceeding. + private void waitForExits() + { + // Kick old flusher forcefully. + setFlusherDone(true); + kickFlusher(); + + if (wg.Count > 0) + { + try + { + Task.WaitAll(this.wg.ToArray()); + } + catch (Exception) { } + } + } + + private void spinUpSocketWatchers() + { + Task t = null; + + waitForExits(); + + t = new Task(() => { readLoop(); }); + t.Start(); + wg.Add(t); + + t = new Task(() => { flusher(); }); + t.Start(); + wg.Add(t); + + lock (mu) + { + this.pout = 0; + + if (Opts.PingInterval > 0) + { + if (ptmr == null) + { + ptmr = new System.Timers.Timer(Opts.PingInterval); + ptmr.Elapsed += pingTimerEventHandler; + ptmr.AutoReset = true; + ptmr.Enabled = true; + ptmr.Start(); + } + else + { + ptmr.Stop(); + ptmr.Interval = Opts.PingInterval; + ptmr.Start(); + } + } + + } + } + + private void pingTimerEventHandler(Object sender, EventArgs args) + { + lock (mu) + { + if (status != ConnState.CONNECTED) + { + return; + } + + pout++; + + if (pout > Opts.MaxPingsOut) + { + processOpError(new NATSStaleConnectionException()); + return; + } + + sendPing(null); + } + } + + public string ConnectedUrl + { + get + { + lock (mu) + { + if (status != ConnState.CONNECTED) + return null; + + return url.OriginalString; + } + } + } + + /// + /// Returns the id of the server currently connected. + /// + public string ConnectedId + { + get + { + lock (mu) + { + if (status != ConnState.CONNECTED) + return IC._EMPTY_; + + return this.info.Id; + } + } + } + + private Queue> createPongs() + { + Queue> rv = new Queue>(); + return rv; + } + + // Process a connected connection and initialize properly. + // Caller must lock. + private void processConnectInit() + { + this.status = ConnState.CONNECTING; + + processExpectedInfo(); + + sendConnect(); + + new Task(() => { spinUpSocketWatchers(); }).Start(); + } + + internal void connect() + { + setupServerPool(); + // Create actual socket connection + // For first connect we walk all servers in the pool and try + // to connect immediately. + bool connected = false; + foreach (Srv s in srvPool) + { + this.url = s.url; + try + { + lastEx = null; + lock (mu) + { + if (createConn()) + { + s.didConnect = true; + s.reconnects = 0; + + processConnectInit(); + + connected = true; + } + } + + } + catch (Exception e) + { + lastEx = e; + close(ConnState.DISCONNECTED, false); + lock (mu) + { + this.url = null; + } + } + + if (connected) + break; + + } // for + + lock (mu) + { + if (this.status != ConnState.CONNECTED) + { + if (this.lastEx == null) + this.lastEx = new NATSNoServersException("Unable to connect to a server."); + + throw this.lastEx; + } + } + } + + // This will check to see if the connection should be + // secure. This can be dictated from either end and should + // only be called after the INIT protocol has been received. + private void checkForSecure() + { + // Check to see if we need to engage TLS + // Check for mismatch in setups + if (Opts.Secure && info.SslRequired) + { + throw new NATSSecureConnWantedException(); + } + else if (info.SslRequired && !Opts.Secure) + { + throw new NATSSecureConnRequiredException(); + } + + // Need to rewrap with bufio + if (Opts.Secure) + { + makeTLSConn(); + } + } + + // processExpectedInfo will look for the expected first INFO message + // sent when a connection is established. The lock should be held entering. + private void processExpectedInfo() + { + Control c; + + try + { + conn.SendTimeout = 2; + c = readOp(); + } + catch (Exception e) + { + processOpError(e); + return; + } + finally + { + conn.SendTimeout = 0; + } + + if (!IC._INFO_OP_.Equals(c.op)) + { + throw new NATSConnectionException("Protocol exception, INFO not received"); + } + + processInfo(c.args); + checkForSecure(); + } + + private void writeString(string format, object a, object b) + { + writeString(String.Format(format, a, b)); + } + + private void writeString(string format, object a, object b, object c) + { + writeString(String.Format(format, a, b, c)); + } + + private void writeString(string value) + { + byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(value); + bw.Write(sendBytes, 0, sendBytes.Length); + } + + private void sendProto(byte[] value, int length) + { + lock (mu) + { + bw.Write(value, 0, length); + } + } + + // Generate a connect protocol message, issuing user/password if + // applicable. The lock is assumed to be held upon entering. + private string connectProto() + { + String u = url.UserInfo; + String user = null; + String pass = null; + + if (!string.IsNullOrEmpty(u)) + { + if (u.Contains(":")) + { + string[] userpass = u.Split(':'); + if (userpass.Length > 0) + { + user = userpass[0]; + } + if (userpass.Length > 1) + { + pass = userpass[1]; + } + } + else + { + user = u; + } + } + + ConnectInfo info = new ConnectInfo(opts.Verbose, opts.Pedantic, user, + pass, opts.Secure, opts.Name); + + StringBuilder sb = new StringBuilder(); + + sb.AppendFormat(IC.conProto, info.ToJson()); + return sb.ToString(); + } + + + // caller must lock. + private void sendConnect() + { + try + { + writeString(connectProto()); + bw.Write(pingProtoBytes, 0, pingProtoBytesLen); + bw.Flush(); + } + catch (Exception ex) + { + if (lastEx == null) + throw new NATSException("Error sending connect protocol message", ex); + } + + string result = null; + try + { + StreamReader sr = new StreamReader(br); + result = sr.ReadLine(); + + // Do not close or dispose the stream reader; + // we need the underlying BufferedStream. + } + catch (Exception ex) + { + throw new NATSConnectionException("Connect read error", ex); + } + + if (IC.pongProtoNoCRLF.Equals(result)) + { + status = ConnState.CONNECTED; + return; + } + else + { + if (result == null) + { + throw new NATSConnectionException("Connect read protocol error"); + } + else if (result.StartsWith(IC._ERR_OP_)) + { + throw new NATSConnectionException( + result.TrimStart(IC._ERR_OP_.ToCharArray())); + } + else if (result.StartsWith("tls:")) + { + throw new NATSSecureConnRequiredException(result); + } + else + { + throw new NATSException(result); + } + } + } + + private Control readOp() + { + // This is only used when creating a connection, so simplify + // life and just create a stream reader to read the incoming + // info string. If this becomes part of the fastpath, read + // the string directly using the buffered reader. + // + // Do not close or dispose the stream reader - we need the underlying + // BufferedStream. + StreamReader sr = new StreamReader(br); + return new Control(sr.ReadLine()); + } + + private void processDisconnect() + { + status = ConnState.DISCONNECTED; + if (lastEx == null) + return; + + if (info.SslRequired) + lastEx = new NATSSecureConnRequiredException(); + else + lastEx = new NATSConnectionClosedException(); + } + + // This will process a disconnect when reconnect is allowed. + // The lock should not be held on entering this function. + private void processReconnect() + { + lock (mu) + { + // If we are already in the proper state, just return. + if (isReconnecting()) + return; + + status = ConnState.RECONNECTING; + + if (ptmr != null) + { + ptmr.Stop(); + } + + if (conn.isSetup()) + { + conn.teardown(); + } + + new Task(() => { doReconnect(); }).Start(); + } + } + + // flushReconnectPending will push the pending items that were + // gathered while we were in a RECONNECTING state to the socket. + private void flushReconnectPendingItems() + { + if (pending == null) + return; + + if (pending.Length > 0) + { + bw.Write(pending.GetBuffer(), 0, (int)pending.Length); + bw.Flush(); + } + + pending = null; + } + + // Try to reconnect using the option parameters. + // This function assumes we are allowed to reconnect. + private void doReconnect() + { + // We want to make sure we have the other watchers shutdown properly + // here before we proceed past this point + waitForExits(); + + + + // FIXME(dlc) - We have an issue here if we have + // outstanding flush points (pongs) and they were not + // sent out, but are still in the pipe. + + // Hold the lock manually and release where needed below. + Monitor.Enter(mu); + + pending = new MemoryStream(); + bw = new BufferedStream(pending); + + // Clear any errors. + lastEx = null; + + if (Opts.DisconnectedEventHandler != null) + { + Monitor.Exit(mu); + + try + { + Opts.DisconnectedEventHandler(this, + new ConnEventArgs(this)); + } + catch (Exception) { } + + Monitor.Enter(mu); + } + + Srv s; + while ((s = selectNextServer()) != null) + { + if (lastEx != null) + break; + + // Sleep appropriate amount of time before the + // connection attempt if connecting to same server + // we just got disconnected from. + double elapsedMillis = s.TimeSinceLastAttempt.TotalMilliseconds; + + if (elapsedMillis < Opts.ReconnectWait) + { + double sleepTime = Opts.ReconnectWait - elapsedMillis; + + Monitor.Exit(mu); + Thread.Sleep((int)sleepTime); + Monitor.Enter(mu); + } + else + { + // Yield so other things like unsubscribes can + // proceed. + Monitor.Exit(mu); + Thread.Sleep((int)50); + Monitor.Enter(mu); + } + + if (isClosed()) + break; + + s.reconnects++; + + try + { + // try to create a new connection + createConn(); + } + catch (Exception) + { + // not yet connected, retry and hold + // the lock. + continue; + } + + // We are reconnected. + stats.reconnects++; + + // Clear out server stats for the server we connected to.. + s.didConnect = true; + + // process our connect logic + try + { + processConnectInit(); + } + catch (Exception e) + { + lastEx = e; + status = ConnState.RECONNECTING; + continue; + } + + s.reconnects = 0; + + // Process CreateConnection logic + try + { + // Send existing subscription state + resendSubscriptions(); + + // Now send off and clear pending buffer + flushReconnectPendingItems(); + + // we are connected. + status = ConnState.CONNECTED; + } + catch (Exception) + { + status = ConnState.RECONNECTING; + continue; + } + + // get the event handler under the lock + ConnEventHandler reconnectedEh = Opts.ReconnectedEventHandler; + + // Release the lock here, we will return below + Monitor.Exit(mu); + + // flush everything + Flush(); + + if (reconnectedEh != null) + { + try + { + reconnectedEh(this, new ConnEventArgs(this)); + } + catch (Exception) { } + } + + return; + + } + + // we have no more servers left to try. + if (lastEx == null) + lastEx = new NATSNoServersException("Unable to reconnect"); + + Monitor.Exit(mu); + + Close(); + } + + private bool isConnecting() + { + return (status == ConnState.CONNECTING); + } + + private void processOpError(Exception e) + { + bool disconnected = false; + + lock (mu) + { + if (isConnecting() || isClosed() || isReconnecting()) + { + return; + } + + if (Opts.AllowReconnect && status == ConnState.CONNECTED) + { + processReconnect(); + } + else + { + processDisconnect(); + disconnected = true; + lastEx = e; + } + } + + if (disconnected) + { + Close(); + } + } + + private void readLoop() + { + // Stack based buffer. + byte[] buffer = new byte[Defaults.defaultReadLength]; + Parser parser = new Parser(this); + int len; + bool sb; + + while (true) + { + sb = false; + lock (mu) + { + sb = (isClosed() || isReconnecting()); + if (sb) + this.ps = parser; + } + + try + { + len = br.Read(buffer, 0, Defaults.defaultReadLength); + parser.parse(buffer, len); + } + catch (Exception e) + { + if (State != ConnState.CLOSED) + { + processOpError(e); + } + break; + } + } + + lock (mu) + { + parser = null; + } + } + + // deliverMsgs waits on the delivery channel shared with readLoop and processMsg. + // It is used to deliver messages to asynchronous subscribers. + internal void deliverMsgs(Channel ch) + { + Msg m; + + while (true) + { + lock (mu) + { + if (isClosed()) + return; + } + + m = ch.get(-1); + if (m == null) + { + // the channel has been closed, exit silently. + return; + } + + // Note, this seems odd message having the sub process itself, + // but this is good for performance. + if (!m.sub.processMsg(m)) + { + lock (mu) + { + removeSub(m.sub); + } + } + } + } + + // Roll our own fast conversion - we know it's the right + // encoding. + char[] convertToStrBuf = new char[Defaults.scratchSize]; + + private string convertToString(byte[] buffer, long length) + { + for (int i = 0; i < length; i++) + { + convertToStrBuf[i] = (char)buffer[i]; + } + + // This is the copy operation for msg arg strings. + return new String(convertToStrBuf, 0, (int)length); + } + + // Here we go ahead and convert the message args into + // strings, numbers, etc. The msgArg object is a temporary + // place to hold them, until we create the message. + // + // These strings, once created, are never copied. + // + internal void processMsgArgs(byte[] buffer, long length) + { + string s = convertToString(buffer, length); + string[] args = s.Split(' '); + + switch (args.Length) + { + case 3: + msgArgs.subject = args[0]; + msgArgs.sid = Convert.ToInt64(args[1]); + msgArgs.reply = null; + msgArgs.size = Convert.ToInt32(args[2]); + break; + case 4: + msgArgs.subject = args[0]; + msgArgs.sid = Convert.ToInt64(args[1]); + msgArgs.reply = args[2]; + msgArgs.size = Convert.ToInt32(args[3]); + break; + default: + throw new NATSException("Unable to parse message arguments: " + s); + } + + if (msgArgs.size < 0) + { + throw new NATSException("Invalid Message - Bad or Missing Size: " + s); + } + } + + + // processMsg is called by parse and will place the msg on the + // appropriate channel for processing. All subscribers have their + // their own channel. If the channel is full, the connection is + // considered a slow subscriber. + internal void processMsg(byte[] msg, long length) + { + bool maxReached = false; + Subscription s; + + lock (mu) + { + stats.inMsgs++; + stats.inBytes += length; + + // In regular message processing, the key should be present, + // so optimize by using an an exception to handle a missing key. + // (as opposed to checking with Contains or TryGetValue) + try + { + s = subs[msgArgs.sid]; + } + catch (Exception) + { + // this can happen when a subscriber is unsubscribing. + return; + } + + lock (s.mu) + { + maxReached = s.tallyMessage(length); + if (maxReached == false) + { + if (!s.addMessage(new Msg(msgArgs, s, msg, length), opts.subChanLen)) + { + processSlowConsumer(s); + } + } // maxreached == false + + } // lock s.mu + + } // lock conn.mu + + if (maxReached) + removeSub(s); + } + + // processSlowConsumer will set SlowConsumer state and fire the + // async error handler if registered. + void processSlowConsumer(Subscription s) + { + if (opts.AsyncErrorEventHandler != null && !s.sc) + { + new Task(() => + { + opts.AsyncErrorEventHandler(this, + new ErrEventArgs(this, s, "Slow Consumer")); + }).Start(); + } + s.sc = true; + } + + private void kickFlusher() + { + lock (flusherLock) + { + if (!flusherKicked) + Monitor.Pulse(flusherLock); + + flusherKicked = true; + } + } + + private bool waitForFlusherKick() + { + lock (flusherLock) + { + if (flusherDone == true) + return false; + + // if kicked before we get here meantime, skip + // waiting. + if (!flusherKicked) + { + Monitor.Wait(flusherLock); + } + + flusherKicked = false; + } + + return true; + } + + private void setFlusherDone(bool value) + { + lock (flusherLock) + { + flusherDone = value; + + if (flusherDone) + kickFlusher(); + } + } + + private bool isFlusherDone() + { + lock (flusherLock) + { + return flusherDone; + } + } + + // flusher is a separate task that will process flush requests for the write + // buffer. This allows coalescing of writes to the underlying socket. + private void flusher() + { + setFlusherDone(false); + + if (conn.Connected == false) + { + return; + } + + while (!isFlusherDone()) + { + bool val = waitForFlusherKick(); + if (val == false) + return; + + lock (mu) + { + if (!isConnected()) + return; + + if (bw.CanWrite) + bw.Flush(); + } + } + } + + // processPing will send an immediate pong protocol response to the + // server. The server uses this mechanism to detect dead clients. + internal void processPing() + { + sendProto(pongProtoBytes, pongProtoBytesLen); + } + + + // processPong is used to process responses to the client's ping + // messages. We use pings for the flush mechanism as well. + internal void processPong() + { + //System.Console.WriteLine("COLIN: Processing pong."); + Channel ch = null; + lock (mu) + { + if (pongs.Count > 0) + ch = pongs.Dequeue(); + + pout = 0; + + if (ch != null) + { + ch.add(true); + } + } + } + + // processOK is a placeholder for processing OK messages. + internal void processOK() + { + // NOOP; + return; + } + + // processInfo is used to parse the info messages sent + // from the server. + internal void processInfo(string info) + { + if (info == null || IC._EMPTY_.Equals(info)) + { + return; + } + + this.info = new ServerInfo(info); + } + + // LastError reports the last error encountered via the Connection. + public Exception LastError + { + get + { + return this.lastEx; + } + } + + // processErr processes any error messages from the server and + // sets the connection's lastError. + internal void processErr(MemoryStream errorStream) + { + bool invokeDelegates = false; + Exception ex = null; + + String s = System.Text.Encoding.UTF8.GetString( + errorStream.ToArray(), 0, (int)errorStream.Position); + + if (IC.STALE_CONNECTION.Equals(s)) + { + processOpError(new NATSStaleConnectionException()); + } + else + { + ex = new NATSException(s); + lock (mu) + { + lastEx = ex; + + if (status != ConnState.CONNECTING) + { + invokeDelegates = true; + } + } + + close(ConnState.CLOSED, invokeDelegates); + } + } + + // publish is the internal function to publish messages to a nats-server. + // Sends a protocol data message by queueing into the bufio writer + // and kicking the flush go routine. These writes should be protected. + private void publish(string subject, string reply, byte[] data) + { + if (string.IsNullOrWhiteSpace(subject)) + { + throw new ArgumentException( + "Subject cannot be null, empty, or whitespace.", + "subject"); + } + + int msgSize = data != null ? data.Length : 0; + + lock (mu) + { + // Proactively reject payloads over the threshold set by server. + if (msgSize > info.MaxPayload) + throw new NATSMaxPayloadException(); + + if (isClosed()) + throw new NATSConnectionClosedException(); + + if (lastEx != null) + throw lastEx; + + // .NET is very performant using string builder. + publishSb.Clear(); + + if (reply == null) + { + publishSb.Append(IC._PUB_P_); + publishSb.Append(" "); + publishSb.Append(subject); + publishSb.Append(" "); + } + else + { + publishSb.Append(IC._PUB_P_ + " " + subject + " " + + reply + " "); + } + + publishSb.Append(msgSize); + publishSb.Append(IC._CRLF_); + + byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(publishSb.ToString()); + bw.Write(sendBytes, 0, sendBytes.Length); + + if (msgSize > 0) + { + bw.Write(data, 0, msgSize); + } + + bw.Write(_CRLF_AS_BYTES, 0, _CRLF_AS_BYTES.Length); + + stats.outMsgs++; + stats.outBytes += msgSize; + + kickFlusher(); + } + + } // publish + + public void Publish(string subject, byte[] data) + { + publish(subject, null, data); + } + + public void Publish(Msg msg) + { + publish(msg.Subject, msg.Reply, msg.Data); + } + + public void Publish(string subject, string reply, byte[] data) + { + publish(subject, reply, data); + } + + private Msg request(string subject, byte[] data, int timeout) + { + Msg m = null; + string inbox = NewInbox(); + + SyncSubscription s = subscribeSync(inbox, null); + s.AutoUnsubscribe(1); + + publish(subject, inbox, data); + m = s.NextMessage(timeout); + try + { + // the auto unsubscribe should handle this. + s.Unsubscribe(); + } + catch (Exception) { /* NOOP */ } + + return m; + } + + public Msg Request(string subject, byte[] data, int timeout) + { + // a timeout of 0 will never succeed - do not allow it. + if (timeout <= 0) + { + throw new ArgumentException( + "Timeout must be greater that 0.", + "timeout"); + } + + return request(subject, data, timeout); + } + + public Msg Request(string subject, byte[] data) + { + return request(subject, data, -1); + } + + public string NewInbox() + { + if (r == null) + r = new Random(); + + byte[] buf = new byte[13]; + + r.NextBytes(buf); + + return IC.inboxPrefix + BitConverter.ToString(buf).Replace("-",""); + } + + internal void sendSubscriptonMessage(AsyncSubscription s) + { + lock (mu) + { + // We will send these for all subs when we reconnect + // so that we can suppress here. + if (!isReconnecting()) + { + writeString(IC.subProto, s.Subject, s.Queue, s.sid); + kickFlusher(); + } + } + } + + private void addSubscription(Subscription s) + { + s.sid = Interlocked.Increment(ref ssid); + subs[s.sid] = s; + } + + private AsyncSubscription subscribeAsync(string subject, string queue) + { + AsyncSubscription s = null; + + lock (mu) + { + if (isClosed()) + throw new NATSConnectionClosedException(); + + s = new AsyncSubscription(this, subject, queue); + + addSubscription(s); + } + + return s; + } + + // subscribe is the internal subscribe + // function that indicates interest in a subject. + private SyncSubscription subscribeSync(string subject, string queue) + { + SyncSubscription s = null; + + lock (mu) + { + if (isClosed()) + throw new NATSConnectionClosedException(); + + s = new SyncSubscription(this, subject, queue); + + addSubscription(s); + + // We will send these for all subs when we reconnect + // so that we can suppress here. + if (!isReconnecting()) + { + writeString(IC.subProto, subject, queue, s.sid); + } + } + + kickFlusher(); + + return s; + } + + public ISyncSubscription SubscribeSync(string subject) + { + return subscribeSync(subject, null); + } + + public IAsyncSubscription SubscribeAsync(string subject) + { + return subscribeAsync(subject, null); + } + + public ISyncSubscription SubscribeSync(string subject, string queue) + { + return subscribeSync(subject, queue); + } + + public IAsyncSubscription SubscribeAsync(string subject, string queue) + { + return subscribeAsync(subject, queue); + } + + // unsubscribe performs the low level unsubscribe to the server. + // Use Subscription.Unsubscribe() + internal void unsubscribe(Subscription sub, int max) + { + lock (mu) + { + if (isClosed()) + throw new NATSConnectionClosedException(); + + Subscription s = subs[sub.sid]; + if (s == null) + { + // already unsubscribed + return; + } + + if (max > 0) + { + s.max = max; + } + else + { + removeSub(s); + } + + // We will send all subscriptions when reconnecting + // so that we can supress here. + if (!isReconnecting()) + writeString(IC.unsubProto, s.sid, max); + + } + + kickFlusher(); + } + + internal void removeSub(Subscription s) + { + Subscription o; + + subs.TryRemove(s.sid, out o); + if (s.mch != null) + { + s.mch.close(); + s.mch = null; + } + + s.conn = null; + } + + // FIXME: This is a hack + // removeFlushEntry is needed when we need to discard queued up responses + // for our pings as part of a flush call. This happens when we have a flush + // call outstanding and we call close. + private bool removeFlushEntry(Channel chan) + { + if (pongs == null) + return false; + + if (pongs.Count == 0) + return false; + + Channel start = pongs.Dequeue(); + Channel c = start; + + while (true) + { + if (c == chan) + { + return true; + } + else + { + pongs.Enqueue(c); + } + + c = pongs.Dequeue(); + + if (c == start) + break; + } + + return false; + } + + // The caller must lock this method. + private void sendPing(Channel ch) + { + if (ch != null) + pongs.Enqueue(ch); + + bw.Write(pingProtoBytes, 0, pingProtoBytesLen); + bw.Flush(); + } + + private void processPingTimer() + { + lock (mu) + { + if (status != ConnState.CONNECTED) + return; + + // Check for violation + this.pout++; + if (this.pout <= Opts.MaxPingsOut) + { + sendPing(null); + + // reset the timer + ptmr.Stop(); + ptmr.Start(); + + return; + } + } + + // if we get here, we've encountered an error. Process + // this outside of the lock. + processOpError(new NATSStaleConnectionException()); + } + + public void Flush(int timeout) + { + if (timeout <= 0) + { + throw new ArgumentOutOfRangeException( + "Timeout must be greater than 0", + "timeout"); + } + + Channel ch = new Channel(1); + lock (mu) + { + if (isClosed()) + throw new NATSConnectionClosedException(); + + sendPing(ch); + } + + try + { + bool rv = ch.get(timeout); + if (!rv) + { + lastEx = new NATSConnectionClosedException(); + } + } + catch (NATSTimeoutException te) + { + lastEx = te; + } + catch (Exception e) + { + lastEx = new NATSException("Flush channel error.", e); + } + + if (lastEx != null) + { + removeFlushEntry(ch); + throw lastEx; + } + } + + /// + /// Flush will perform a round trip to the server and return when it + /// receives the internal reply. + /// + public void Flush() + { + // 60 second default. + Flush(60000); + } + + // resendSubscriptions will send our subscription state back to the + // server. Used in reconnects + private void resendSubscriptions() + { + foreach (Subscription s in subs.Values) + { + if (s is IAsyncSubscription) + ((AsyncSubscription)s).enableAsyncProcessing(); + + + writeString(IC.subProto, s.Subject, s.Queue, s.sid); + } + + bw.Flush(); + } + + + // Clear pending flush calls and reset + private void resetPendingFlush() + { + lock (mu) + { + clearPendingFlushCalls(); + this.pongs = createPongs(); + } + } + + // This will clear any pending flush calls and release pending calls. + private void clearPendingFlushCalls() + { + lock (mu) + { + // Clear any queued pongs, e.g. pending flush calls. + foreach (Channel ch in pongs) + { + if (ch != null) + ch.add(true); + } + + pongs.Clear(); + } + } + + + // Low level close call that will do correct cleanup and set + // desired status. Also controls whether user defined callbacks + // will be triggered. The lock should not be held entering this + // function. This function will handle the locking manually. + private void close(ConnState closeState, bool invokeDelegates) + { + ConnEventHandler disconnectedEventHandler = null; + ConnEventHandler closedEventHandler = null; + + lock (mu) + { + if (isClosed()) + { + status = closeState; + return; + } + + status = ConnState.CLOSED; + } + + // Kick the routines so they fall out. + // fch will be closed on finalizer + kickFlusher(); + + // Clear any queued pongs, e.g. pending flush calls. + clearPendingFlushCalls(); + + lock (mu) + { + if (ptmr != null) + ptmr.Stop(); + + // Close sync subscriber channels and release any + // pending NextMsg() calls. + foreach (Subscription s in subs.Values) + { + s.closeChannel(); + } + + subs.Clear(); + + // perform appropriate callback is needed for a + // disconnect; + if (invokeDelegates && conn.isSetup() && + Opts.DisconnectedEventHandler != null) + { + // TODO: Mirror go, but this can result in a callback + // being invoked out of order + disconnectedEventHandler = Opts.DisconnectedEventHandler; + new Task(() => { disconnectedEventHandler(this, new ConnEventArgs(this)); }).Start(); + } + + // Go ahead and make sure we have flushed the outbound buffer. + status = ConnState.CLOSED; + if (conn.isSetup()) + { + if (bw != null) + bw.Flush(); + + conn.teardown(); + } + + closedEventHandler = opts.ClosedEventHandler; + } + + if (invokeDelegates && closedEventHandler != null) + { + try + { + closedEventHandler(this, new ConnEventArgs(this)); + } + catch (Exception) { } + } + + lock (mu) + { + status = closeState; + } + } + + public void Close() + { + close(ConnState.CLOSED, true); + } + + // assume the lock is head. + private bool isClosed() + { + return (status == ConnState.CLOSED); + } + + public bool IsClosed() + { + lock (mu) + { + return isClosed(); + } + } + + public bool IsReconnecting() + { + lock (mu) + { + return isReconnecting(); + } + } + + public ConnState State + { + get + { + lock (mu) + { + return status; + } + } + } + + private bool isReconnecting() + { + lock (mu) + { + return (status == ConnState.RECONNECTING); + } + } + + // Test if Conn is connected or connecting. + private bool isConnected() + { + return (status == ConnState.CONNECTING || status == ConnState.CONNECTED); + } + + public IStatistics Stats + { + get + { + lock (mu) + { + return new Statistics(this.stats); + } + } + } + + public void ResetStats() + { + lock (mu) + { + this.stats.clear(); + } + } + + public long MaxPayload + { + get + { + lock (mu) + { + return info.MaxPayload; + } + } + } + + /// + /// Returns a string representation of the + /// value of this Connection instance. + /// + /// String value of this instance. + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("{"); + sb.AppendFormat("url={0};", url); + sb.AppendFormat("info={0};", info); + sb.AppendFormat("status={0}", status); + sb.Append("Subscriptions={"); + foreach (Subscription s in subs.Values) + { + sb.Append("Subscription {" + s.ToString() + "}"); + } + sb.Append("}}"); + + return sb.ToString(); + } + + void IDisposable.Dispose() + { + try + { + Close(); + } + catch (Exception) + { + // No need to throw an exception here + } + } + } // class Conn + + +} diff --git a/NATS/ConnectionFactory.cs b/NATS/ConnectionFactory.cs new file mode 100755 index 000000000..1f9d6d966 --- /dev/null +++ b/NATS/ConnectionFactory.cs @@ -0,0 +1,79 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NATS.Client +{ + /// + /// Creates a connection to the NATS server. + /// + public sealed class ConnectionFactory + { + /// + /// Creates a connection factory to the NATS server. + /// + public ConnectionFactory() { } + + /// + /// CreateConnection will attempt to connect to the NATS server. + /// The url can contain username/password semantics. + /// + /// The url + /// A new connection to the NATS server + public IConnection CreateConnection(string url) + { + Options opts = new Options(); + opts.Url = url; + return CreateConnection(opts); + } + + /// + /// Retrieves the default set ot client options. + /// + public static Options GetDefaultOptions() + { + return new Options(); + } + + /// + /// CreateSecureConnection will attempt to connect to the NATS server using TLS. + /// The url can contain username/password semantics. + /// + /// connect url + /// A new connection to the NATS server + public IConnection CreateSecureConnection(string url) + { + Options opts = new Options(); + opts.Url = url; + opts.Secure = true; + return CreateConnection(opts); + } + + /// + /// Create a connection to the NATs server using default options. + /// + /// A new connection to the NATS server + public IConnection CreateConnection() + { + return CreateConnection(new Options()); + } + + /// + /// CreateConnection to the NATs server using the provided options. + /// + /// NATs client options + /// A new connection to the NATS server + public IConnection CreateConnection(Options opts) + { + Connection nc = new Connection(opts); + nc.connect(); + return nc; + } + + + } +} diff --git a/NATS/Exceptions.cs b/NATS/Exceptions.cs new file mode 100755 index 000000000..abc7e5e9f --- /dev/null +++ b/NATS/Exceptions.cs @@ -0,0 +1,123 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading; +using System.IO; + + +namespace NATS.Client +{ + /// + /// The exception that is thrown when there is a NATS error condition. All + /// NATS exception inherit from this class. + /// + public class NATSException : Exception + { + internal NATSException() : base() { } + internal NATSException(string err) : base (err) {} + internal NATSException(string err, Exception innerEx) : base(err, innerEx) { } + } + + /// + /// The exception that is thrown when there is a connection error. + /// + public class NATSConnectionException : NATSException + { + internal NATSConnectionException(string err) : base(err) { } + internal NATSConnectionException(string err, Exception innerEx) : base(err, innerEx) { } + } + + /// + /// This exception that is thrown when there is an internal error with + /// the NATS protocol. + /// + public class NATSProtocolException : NATSException + { + internal NATSProtocolException(string err) : base(err) { } + } + + /// + /// The exception that is thrown when a connection cannot be made + /// to any server. + /// + public class NATSNoServersException : NATSException + { + internal NATSNoServersException(string err) : base(err) { } + } + + /// + /// The exception that is thrown when a secure connection is requested, + /// but not required. + /// + public class NATSSecureConnWantedException : NATSException + { + internal NATSSecureConnWantedException() : base("A secure connection is requested.") { } + } + + /// + /// The exception that is thrown when a secure connection is required. + /// + public class NATSSecureConnRequiredException : NATSException + { + internal NATSSecureConnRequiredException() : base("A secure connection is required.") { } + internal NATSSecureConnRequiredException(String s) : base(s) { } + } + + /// + /// The exception that is thrown when a an operation is performed on + /// a connection that is closed. + /// + public class NATSConnectionClosedException : NATSException + { + internal NATSConnectionClosedException() : base("Connection is closed.") { } + } + + /// + /// The exception that is thrown when a consumer (subscription) is slow. + /// + public class NATSSlowConsumerException : NATSException + { + internal NATSSlowConsumerException() : base("Consumer is too slow.") { } + } + + /// + /// The exception that is thrown when an operation occurs on a connection + /// that has been determined to be stale. + /// + public class NATSStaleConnectionException : NATSException + { + internal NATSStaleConnectionException() : base("Connection is stale.") { } + } + + /// + /// The exception that is thrown when a message payload exceeds what + /// the maximum configured. + /// + public class NATSMaxPayloadException : NATSException + { + internal NATSMaxPayloadException() : base("Maximum payload size has been exceeded") { } + internal NATSMaxPayloadException(string err) : base(err) { } + } + + /// + /// The exception that is thrown when a subscriber operation is performed on + /// an invalid subscriber. + /// + public class NATSBadSubscriptionException : NATSException + { + internal NATSBadSubscriptionException() : base("Subcription is not valid.") { } + } + + /// + /// The exception that is thrown when a NATS operation times out. + /// + public class NATSTimeoutException : NATSException + { + internal NATSTimeoutException() : base("Timeout occurred.") { } + } +} \ No newline at end of file diff --git a/NATS/IAsyncSubscription.cs b/NATS/IAsyncSubscription.cs new file mode 100755 index 000000000..1ce768da6 --- /dev/null +++ b/NATS/IAsyncSubscription.cs @@ -0,0 +1,34 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NATS.Client +{ + /// + /// An object of this class is an asynchronous subscription representing interest + /// in a subject. The subject can have wildcards (partial:*, full:>). + /// Messages will be delivered to the associated MsgHandler event delegates. + /// While nothing prevents event handlers from being added or + /// removed while processing messages, no messages will be received until + /// Start() has been called. This allows all event handlers to be added + /// before message processing begins. + /// + /// See MsgHandler. + public interface IAsyncSubscription : ISubscription, IDisposable + { + /// + /// Adds or removes a message handlers for this subscriber. + /// + /// See MsgHandler + event MsgHandler MessageHandler; + + /// + /// This completes the subsciption process notifying the server this subscriber + /// has interest. + /// + void Start(); + } +} diff --git a/NATS/IConnection.cs b/NATS/IConnection.cs new file mode 100755 index 000000000..39c12127d --- /dev/null +++ b/NATS/IConnection.cs @@ -0,0 +1,186 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NATS.Client +{ + /// + /// Represents the connection to the NATS server. + /// + public interface IConnection : IDisposable + { + /// + /// Returns the options used to create this connection. + /// + Options Opts { get; } + + /// + /// Returns the url of the server currently connected, null otherwise. + /// + string ConnectedUrl { get; } + + /// + /// Returns the id of the server currently connected. + /// + string ConnectedId { get; } + + /// + /// LastError reports the last error encountered via the Connection. + /// + Exception LastError { get; } + + /// + /// Publish publishes the data argument to the given subject. The data + /// argument is left untouched and needs to be correctly interpreted on + /// the receiver. + /// + /// Subject to publish the message to. + /// Message payload + void Publish(string subject, byte[] data); + + /// + /// Publishes the Msg structure, which includes the + /// Subject, an optional Reply and an optional Data field. + /// + /// The message to send. + void Publish(Msg msg); + + /// + /// Publish will perform a Publish() excpecting a response on the + /// reply subject. Use Request() for automatically waiting for a response + /// inline. + /// + /// Subject to publish on + /// Subject the receiver will on. + /// The message payload + void Publish(string subject, string reply, byte[] data); + + /// + /// Request will create an Inbox and perform a Request() call + /// with the Inbox reply and return the first reply received. + /// This is optimized for the case of multiple responses. + /// + /// + /// A negative timeout blocks forever, zero is not allowed. + /// + /// Subject to send the request on. + /// payload of the message + /// time to block + Msg Request(string subject, byte[] data, int timeout); + + /// + /// Request will create an Inbox and perform a Request() call + /// with the Inbox reply and return the first reply received. + /// This is optimized for the case of multiple responses. + /// + /// Subject to send the request on. + /// payload of the message + Msg Request(string subject, byte[] data); + + /// + /// NewInbox will return an inbox string which can be used for directed replies from + /// subscribers. These are guaranteed to be unique, but can be shared and subscribed + /// to by others. + /// + /// A string representing an inbox. + string NewInbox(); + + /// + /// Subscribe will create a subscriber with interest in a given subject. + /// The subject can have wildcards (partial:*, full:>). Messages will be delivered + /// to the associated MsgHandler. If no MsgHandler is set, the + /// subscription is a synchronous subscription and can be polled via + /// Subscription.NextMsg(). Subscriber message handler delegates + /// can be added or removed anytime. + /// + /// Subject of interest. + /// A new Subscription + ISyncSubscription SubscribeSync(string subject); + + /// + /// SubscribeAsynchronously will create an AsynchSubscriber with + /// interest in a given subject. + /// + /// Subject of interest. + /// A new Subscription + IAsyncSubscription SubscribeAsync(string subject); + + /// + /// Creates a synchronous queue subscriber on the given + /// subject. All subscribers with the same queue name will form the queue + /// group and only one member of the group will be selected to receive any + /// given message synchronously. + /// + /// Subject of interest + /// Name of the queue group + /// A new Subscription + ISyncSubscription SubscribeSync(string subject, string queue); + + /// + /// This method creates an asynchronous queue subscriber on the given subject. + /// All subscribers with the same queue name will form the queue group and + /// only one member of the group will be selected to receive any given + /// message asynchronously. + /// + /// Subject of interest + /// Name of the queue group + /// A new Subscription + IAsyncSubscription SubscribeAsync(string subject, string queue); + + /// + /// Flush will perform a round trip to the server and return when it + /// receives the internal reply. + /// + /// The timeout in milliseconds. + void Flush(int timeout); + + /// + /// Flush will perform a round trip to the server and return when it + /// receives the internal reply. + /// + void Flush(); + + /// + /// Close will close the connection to the server. This call will release + /// all blocking calls, such as Flush() and NextMsg(). + /// + void Close(); + + /// + /// Test if this connection has been closed. + /// + /// true if closed, false otherwise. + bool IsClosed(); + + + /// + /// Test if this connection is reconnecting. + /// + /// true if reconnecting, false otherwise. + bool IsReconnecting(); + + /// + /// Gets the current state of the connection. + /// + ConnState State { get; } + + // Stats will return a race safe copy of connection statistics. + /// + /// Returns a race safe copy of connection statistics. + /// + IStatistics Stats { get; } + + /// + /// Resets connection statistics. + /// + void ResetStats(); + + /// + /// Returns the server defined size limit that a message payload can have. + /// + long MaxPayload { get; } + } +} diff --git a/NATS/IStatistics.cs b/NATS/IStatistics.cs new file mode 100755 index 000000000..d64396e75 --- /dev/null +++ b/NATS/IStatistics.cs @@ -0,0 +1,40 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NATS.Client +{ + /// + /// Tracks various statistics received and sent on this connection. + /// + public interface IStatistics + { + /// + /// Gets the number of inbound messages received. + /// + long InMsgs { get; } + + /// + /// Gets the number of messages sent. + /// + long OutMsgs { get; } + + /// + /// Gets the number of incoming bytes. + /// + long InBytes { get; } + + /// + /// Gets the outgoing number of bytes. + /// + long OutBytes { get; } + + /// + /// Gets the number of reconnections. + /// + long Reconnects { get; } + } +} diff --git a/NATS/ISubscription.cs b/NATS/ISubscription.cs new file mode 100755 index 000000000..0a2f0e041 --- /dev/null +++ b/NATS/ISubscription.cs @@ -0,0 +1,62 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NATS.Client +{ + /// + /// Represents interest in a NATS topic. + /// + public interface ISubscription + { + /// + /// Gets the subject of interest. + /// + string Subject { get; } + + /// + /// Gets the name of the queue groups this subscriber belongs to. + /// + /// + /// Optional queue group name. If present, all subscriptions with the + /// same name will form a distributed queue, and each message will + /// only be processed by one member of the group. + /// + string Queue { get; } + + + /// + /// Gets the Connection this subscriber was created on. + /// + Connection Connection { get; } + + /// + /// True if the subscription is active, false otherwise. + /// + bool IsValid { get; } + + + /// + /// Removes interest in the given subject. + /// + void Unsubscribe(); + + /// + /// AutoUnsubscribe will issue an automatic Unsubscribe that is + /// processed by the server when max messages have been received. + /// This can be useful when sending a request to an unknown number + /// of subscribers. Request() uses this functionality. + /// + /// Number of messages to receive before unsubscribing. + void AutoUnsubscribe(int max); + + /// + /// Gets the number of messages received, but not processed, + /// this subscriber. + /// + int QueuedMessageCount { get; } + } +} diff --git a/NATS/ISyncSubscription.cs b/NATS/ISyncSubscription.cs new file mode 100755 index 000000000..3f7c043b0 --- /dev/null +++ b/NATS/ISyncSubscription.cs @@ -0,0 +1,36 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NATS.Client +{ + /// + /// A Syncronous Subscripion will express interest in a given subject. + /// The subject can have wildcards (partial:*, full:>). + /// Messages arriving are retrieved via NextMsg() + /// + public interface ISyncSubscription : ISubscription, IDisposable + { + /// + /// This method will return the next message available to a synchronous subscriber + /// or block until one is available. + /// + /// a NATS message + Msg NextMessage(); + + /// + /// This method will return the next message available to a synchronous subscriber + /// or block until one is available. A timeout can be used to return when no + /// message has been delivered. + /// + /// + /// A timeout of 0 will return null immediately if there are no messages. + /// + /// Timeout value + /// a NATS message + Msg NextMessage(int timeout); + } +} diff --git a/NATS/Msg.cs b/NATS/Msg.cs new file mode 100755 index 000000000..1abdad1bd --- /dev/null +++ b/NATS/Msg.cs @@ -0,0 +1,188 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Text; + +namespace NATS.Client +{ + /// + /// A NATS message is an object encapsulating a subject, optional reply + /// payload, and subscription information, sent or received by teh client + /// application. + /// + public sealed class Msg + { + private string subject; + private string reply; + private byte[] data; + internal Subscription sub; + + /// + /// Creates an empty message. + /// + public Msg() + { + subject = null; + reply = null; + data = null; + sub = null; + } + + private void init(string subject, string reply, byte[] data) + { + if (string.IsNullOrWhiteSpace(subject)) + { + throw new ArgumentException( + "Subject cannot be null, empty, or whitespace.", + "subject"); + } + + this.Subject = subject; + this.Reply = reply; + this.Data = data; + } + + /// + /// Creates a message with a subject, reply, and data. + /// + /// Subject of the message, required. + /// Reply subject, can be null. + /// Message payload + public Msg(string subject, string reply, byte[] data) + { + init(subject, reply, data); + } + + /// + /// Creates a message with a subject and data. + /// + /// Subject of the message, required. + /// Message payload + public Msg(string subject, byte[] data) + { + init(subject, null, data); + } + + /// + /// Creates a message with a subject and no payload. + /// + /// Subject of the message, required. + public Msg(string subject) + { + init(subject, null, null); + } + + internal Msg(MsgArg arg, Subscription s, byte[] payload, long length) + { + this.subject = arg.subject; + this.reply = arg.reply; + this.sub = s; + + // make a deep copy of the bytes for this message. + this.data = new byte[length]; + Array.Copy(payload, this.data, length); + } + + /// + /// Gets or sets the subject. + /// + public string Subject + { + get { return subject; } + set { subject = value; } + } + + /// + /// Gets or sets the reply subject. + /// + public string Reply + { + get { return reply; } + set { reply = value; } + } + + /// + /// Sets data in the message. This copies application data into the message. + /// + /// + /// See AssignData to directly pass the bytes + /// buffer. + /// + /// + public byte[] Data + { + get { return data; } + + set + { + if (value == null) + { + this.data = null; + return; + } + + int len = value.Length; + if (len == 0) + this.data = null; + else + { + this.data = new byte[len]; + Array.Copy(value, data, len); + } + } + } + + /// + /// Assigns the data of the message. This is a direct assignment, + /// to avoid expensive copy operations. A change to the passed + /// byte array will be changed in the message. + /// + /// + /// The application is responsible for the data integrity in the message. + /// + /// a bytes buffer of data. + public void AssignData(byte[] data) + { + this.data = data; + } + + /// + /// Gets the subscription assigned to the messages. + /// + public Subscription ArrivalSubcription + { + get { return sub; } + } + + /// + /// Generates a string representation of the messages. + /// + /// A string representation of the messages. + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("{"); + sb.AppendFormat("Subject={0};Reply={1};Payload=<", Subject, + Reply != null ? reply : "null"); + + int len = data.Length; + int i; + + for (i = 0; i < 32 && i < len; i++) + { + sb.Append((char)data[i]); + } + + if (i < len) + { + sb.AppendFormat("{0} more bytes", len - i); + } + + sb.Append(">}"); + + return sb.ToString(); + } + } + + +} \ No newline at end of file diff --git a/NATS/NATS.cs b/NATS/NATS.cs new file mode 100755 index 000000000..33de0bded --- /dev/null +++ b/NATS/NATS.cs @@ -0,0 +1,251 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + + +// This is the NATS .NET client. +// +// This Apcera supported client library follows the go client closely, +// diverging where it makes sense to follow the common design +// semantics of the language. +// +// While public and protected methods +// and properties adhere to the .NET coding guidlines, +// internal/private members and methods mirror the go client for +// maintenance purposes. Public method and properties are +// documented with standard .NET doc. +// +// - Public/Protected members and methods are in PascalCase +// - Public/Protected members and methods are documented in +// standard .NET documentation. +// - Private/Internal members and methods are in camelCase. +// - There are no "callbacks" - delegates only. +// - Public members are accessed through a property. +// - When possible, internal members are accessed directly. +// - Internal Variable Names mirror those of the go client. +// - A minimal/no reliance on third party packages. +// +// Coding guidelines are based on: +// http://blogs.msdn.com/b/brada/archive/2005/01/26/361363.aspx +// although method location mirrors the go client to faciliate +// maintenance. +// +namespace NATS.Client +{ + /// + /// This class contains default values for fields used throughout NATS. + /// + public static class Defaults + { + /// + /// Client version + /// + public const string Version = "0.0.1"; + + /// + /// The default NATS connect url ("nats://localhost:4222") + /// + public const string Url = "nats://localhost:4222"; + + /// + /// The default NATS connect port. (4222) + /// + public const int Port = 4222; + + /// + /// Default number of times to attempt a reconnect. (60) + /// + public const int MaxReconnect = 60; + + /// + /// Default ReconnectWait time (2 seconds) + /// + public const int ReconnectWait = 2000; // 2 seconds. + + /// + /// Default timeout (2 seconds). + /// + public const int Timeout = 2000; // 2 seconds. + + /// + /// Default ping interval (2 minutes); + /// + public const int PingInterval = 120000;// 2 minutes. + + /// + /// Default MaxPingOut value (2); + /// + public const int MaxPingOut = 2; + + /// + /// Default MaxChanLen (65536) + /// + public const int MaxChanLen = 65536; + + /// + /// Default Request Channel Length + /// + public const int RequestChanLen = 4; + + /// + /// Language string of this client, ".NET" + /// + public const string LangString = ".NET"; + + /* + * Namespace level defaults + */ + + // Scratch storage for assembling protocol headers + internal const int scratchSize = 512; + + // The size of the bufio reader/writer on top of the socket. + // .NET perform better with small buffer sizes. + internal const int defaultBufSize = 32512; + internal const int defaultReadLength = 512; + + // The size of the bufio while we are reconnecting + internal const int defaultPendingSize = 1024 * 1024; + + // Default server pool size + internal const int srvPoolSize = 4; + } + + /// + /// Event arguments for the ConnEventHandler type delegate. + /// + public class ConnEventArgs + { + private Connection c; + + internal ConnEventArgs(Connection c) + { + this.c = c; + } + + /// + /// Gets the connection associated with the event. + /// + public Connection Conn + { + get { return c; } + } + } + + /// + /// Event arguments for the ErrorEventHandler type delegate. + /// + public class ErrEventArgs + { + private Connection c; + private Subscription s; + private String err; + + internal ErrEventArgs(Connection c, Subscription s, String err) + { + this.c = c; + this.s = s; + this.err = err; + } + + /// + /// Gets the connection associated with the event. + /// + public Connection Conn + { + get { return c; } + } + + /// + /// Gets the Subscription associated wit the event. + /// + public Subscription Subscription + { + get { return s; } + } + + /// + /// Gets the error associated with the event. + /// + public string Error + { + get { return err; } + } + + } + + /// + /// Delegate to handle a connection related event. + /// + /// Sender object. + /// Event arguments + public delegate void ConnEventHandler(object sender, ConnEventArgs e); + + /// + /// Delegate to handle error events. + /// + /// Sender object. + /// Sender object. + public delegate void ErrorEventHandler(object sender, ErrEventArgs e); + + /** + * Internal Constants + */ + internal class IC + { + internal const string _CRLF_ = "\r\n"; + internal const string _EMPTY_ = ""; + internal const string _SPC_ = " "; + internal const string _PUB_P_ = "PUB "; + + internal const string _OK_OP_ = "+OK"; + internal const string _ERR_OP_ = "-ERR"; + internal const string _MSG_OP_ = "MSG"; + internal const string _PING_OP_ = "PING"; + internal const string _PONG_OP_ = "PONG"; + internal const string _INFO_OP_ = "INFO"; + + internal const string inboxPrefix = "_INBOX."; + + internal const string conProto = "CONNECT {0}" + IC._CRLF_; + internal const string pingProto = "PING" + IC._CRLF_; + internal const string pongProto = "PONG" + IC._CRLF_; + internal const string pubProto = "PUB {0} {1} {2}" + IC._CRLF_; + internal const string subProto = "SUB {0} {1} {2}" + IC._CRLF_; + internal const string unsubProto = "UNSUB {0} {1}" + IC._CRLF_; + + internal const string pongProtoNoCRLF = "PONG"; + + internal const string STALE_CONNECTION = "Stale Connection"; + } + + /// + /// This class is passed into the MsgHandler delegate, providing the + /// message received. + /// + public class MsgHandlerEventArgs + { + internal Msg msg = null; + + /// + /// Retrieves the message. + /// + public Msg Message + { + get { return msg; } + } + } + + /// + /// This delegate handles event raised when a message arrives. + /// + /// Sender object. + /// MsgHandlerEventArgs + public delegate void MsgHandler(object sender, MsgHandlerEventArgs args); +} diff --git a/NATS/NATS.csproj b/NATS/NATS.csproj new file mode 100755 index 000000000..dcefcc4fd --- /dev/null +++ b/NATS/NATS.csproj @@ -0,0 +1,73 @@ + + + + + Debug + AnyCPU + {68EE71D4-9532-470E-B5CA-ECAA79936B1F} + Library + Properties + NATS.Client + NATS.Client + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NATS/Options.cs b/NATS/Options.cs new file mode 100755 index 000000000..a3e124f3d --- /dev/null +++ b/NATS/Options.cs @@ -0,0 +1,257 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Text; + +namespace NATS.Client +{ + /// + /// This class is used to setup all NATs client options. + /// + public sealed class Options + { + string url = null; + string[] servers = null; + bool noRandomize = false; + String name = null; + bool verbose = false; + bool pedantic = false; + bool secure = false; + bool allowReconnect = true; + int maxReconnect = Defaults.MaxReconnect; + int reconnectWait = Defaults.ReconnectWait; + int pingInterval = Defaults.PingInterval; + int timeout = Defaults.Timeout; + + /// + /// Represents the method that will handle an event raised + /// when a connection is closed. + /// + public ConnEventHandler ClosedEventHandler = null; + + /// + /// Represents the method that will handle an event raised + /// when a connection has been disconnected from a server. + /// + public ConnEventHandler DisconnectedEventHandler = null; + + /// + /// Represents the method that will handle an event raised + /// when a connection has reconnected to a server. + /// + public ConnEventHandler ReconnectedEventHandler = null; + + /// + /// Represents the method that will handle an event raised + /// when an error occurs out of band. + /// + public ErrorEventHandler AsyncErrorEventHandler = null; + + internal int maxPingsOut = Defaults.MaxPingOut; + + internal int subChanLen = 40000; + + // Options can only be created through ConnectionFactory.GetDefaultOptions(); + internal Options() { } + + /// + /// Gets or sets the url used to connect to the NATs server. This may + /// contain user information. + /// + public string Url + { + get { return this.url; } + set { this.url = value; } + } + + /// + /// Gets or Sets the array of servers that the NATs client will connect to. + /// + public string[] Servers + { + get { return this.servers; } + set { this.servers = value; } + } + + /// + /// Gets or Sets the randomization of choosing a server to connect to. + /// + public bool NoRandomize + { + get { return this.noRandomize; } + set { this.noRandomize = value; } + } + + /// + /// Gets or sets the name of this client. + /// + public string Name + { + get { return this.name; } + set { this.name = value; } + } + + /// + /// Gets or sets the verbosity of logging. + /// + public bool Verbose + { + get { return this.verbose; } + set { this.verbose = value; } + } + + /// + /// N/A. + /// + public bool Pedantic + { + get { return this.pedantic; } + set { this.pedantic = value; } + } + + /// + /// Get or sets the secure property. Not currently implemented. + /// + public bool Secure + { + get { return this.secure; } + set { this.secure = value; } + } + + /// + /// Gets or Sets the allow reconnect flag. When set to false, + /// the NATs client will not attempt to reconnect if a connection + /// has been lost. + /// + public bool AllowReconnect + { + get { return this.allowReconnect; } + set { this.allowReconnect = value; } + } + + /// + /// Gets or sets the maxmimum number of times a connection will + /// attempt to reconnect. + /// + public int MaxReconnect + { + get { return this.maxReconnect; } + set { this.maxReconnect = value; } + } + + /// + /// Gets or Sets the amount of time, in milliseconds, the client will + /// wait during a reconnection. + /// + public int ReconnectWait + { + get { return this.reconnectWait; } + set { this.reconnectWait = value; } + } + + /// + /// Gets or sets the interval pings will be sent to the server. + /// Take care to coordinate this value with the server's interval. + /// + public int PingInterval + { + get { return this.pingInterval; } + set { this.pingInterval = value; } + } + + /// + /// Gets or sets the timeout when flushing a connection. + /// + public int Timeout + { + get { return this.timeout; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException( + "Timeout must be zero or greater."); + } + + this.timeout = value; + } + } + + /// + /// Gets or sets the maximum number of outstanding pings before + /// terminating a connection. + /// + public int MaxPingsOut + { + get { return this.maxPingsOut; } + set { this.maxPingsOut = value; } + } + + /// + /// Gets or sets the size of the subscriber channel, or number + /// of messages the subscriber will buffer internally. + /// + public int SubChannelLength + { + get { return this.subChanLen; } + set { this.subChanLen = value; } + } + + private void appendEventHandler(StringBuilder sb, String name, Delegate eh) + { + if (eh != null) + sb.AppendFormat("{0}={1};", name, eh.Method.Name); + else + sb.AppendFormat("{0}=null;", name); + } + + /// + /// Returns a string representation of the + /// value of this Options instance. + /// + /// String value of this instance. + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + sb.Append("{"); + sb.AppendFormat("AllowReconnect={0};", allowReconnect); + + appendEventHandler(sb, "AsyncErrorEventHandler", AsyncErrorEventHandler); + appendEventHandler(sb, "ClosedEventHandler", ClosedEventHandler); + appendEventHandler(sb, "DisconnectedEventHandler", DisconnectedEventHandler); + + sb.AppendFormat("MaxPingsOut={0};", MaxPingsOut); + sb.AppendFormat("MaxReconnect={0};", MaxReconnect); + sb.AppendFormat("Name={0};", Name == null ? Name : "null"); + sb.AppendFormat("NoRandomize={0};", NoRandomize); + sb.AppendFormat("Pendantic={0};", Pedantic); + sb.AppendFormat("PingInterval={0};", PingInterval); + sb.AppendFormat("ReconnectWait={0};", ReconnectWait); + sb.AppendFormat("Secure={0};", Secure); + + if (Servers == null) + { + sb.AppendFormat("Servers=null;"); + } + else + { + sb.Append("Servers={"); + foreach (string s in servers) + { + sb.AppendFormat("[{0}]", s); + if (s != servers[servers.Length-1]) + sb.AppendFormat(","); + } + sb.Append("}"); + } + sb.AppendFormat("SubChannelLength={0};", SubChannelLength); + sb.AppendFormat("Timeout={0};", Timeout); + sb.AppendFormat("Pendantic={0}", Pedantic); + sb.Append("}"); + + return sb.ToString(); + } + } +} + diff --git a/NATS/Parser.cs b/NATS/Parser.cs new file mode 100755 index 000000000..7f111d065 --- /dev/null +++ b/NATS/Parser.cs @@ -0,0 +1,420 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace NATS.Client +{ + internal sealed class MsgArg + { + internal string subject; + internal string reply; + internal long sid; + internal int size; + } + + internal sealed class Parser + { + + Connection conn; + byte[] argBufBase = new byte[Defaults.defaultBufSize]; + MemoryStream argBufStream = null; + + byte[] msgBufBase = new byte[Defaults.defaultBufSize]; + MemoryStream msgBufStream = null; + + internal Parser(Connection conn) + { + argBufStream = new MemoryStream(argBufBase); + msgBufStream = new MemoryStream(msgBufBase); + + this.conn = conn; + } + + internal int state = 0; + + private const int MAX_CONTROL_LINE_SIZE = 1024; + + // For performance declare these as consts - they'll be + // baked into the IL code (thus faster). An enum would + // be nice, but we want speed in this critical section of + // message handling. + private const int OP_START = 0; + private const int OP_PLUS = 1; + private const int OP_PLUS_O = 2; + private const int OP_PLUS_OK = 3; + private const int OP_MINUS = 4; + private const int OP_MINUS_E = 5; + private const int OP_MINUS_ER = 6; + private const int OP_MINUS_ERR = 7; + private const int OP_MINUS_ERR_SPC = 8; + private const int MINUS_ERR_ARG = 9; + private const int OP_C = 10; + private const int OP_CO = 11; + private const int OP_CON = 12; + private const int OP_CONN = 13; + private const int OP_CONNE = 14; + private const int OP_CONNEC = 15; + private const int OP_CONNECT = 16; + private const int CONNECT_ARG = 17; + private const int OP_M = 18; + private const int OP_MS = 19; + private const int OP_MSG = 20; + private const int OP_MSG_SPC = 21; + private const int MSG_ARG = 22; + private const int MSG_PAYLOAD = 23; + private const int MSG_END = 24; + private const int OP_P = 25; + private const int OP_PI = 26; + private const int OP_PIN = 27; + private const int OP_PING = 28; + private const int OP_PO = 29; + private const int OP_PON = 30; + private const int OP_PONG = 31; + + private void parseError(byte[] buffer, int position) + { + throw new NATSException(string.Format("Parse Error [{0}], {1}", state, buffer)); + } + + internal void parse(byte[] buffer, int len) + { + int i; + char b; + + for (i = 0; i < len; i++) + { + b = (char)buffer[i]; + + switch (state) + { + case OP_START: + switch (b) + { + case 'M': + case 'm': + state = OP_M; + break; + case 'C': + case 'c': + state = OP_C; + break; + case 'P': + case 'p': + state = OP_P; + break; + case '+': + state = OP_PLUS; + break; + case '-': + state = OP_MINUS; + break; + default: + parseError(buffer,i); + break; + } + break; + case OP_M: + switch (b) + { + case 'S': + case 's': + state = OP_MS; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MS: + switch (b) + { + case 'G': + case 'g': + state = OP_MSG; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MSG: + switch (b) + { + case ' ': + case '\t': + state = OP_MSG_SPC; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MSG_SPC: + switch (b) + { + case ' ': + break; + case '\t': + break; + default: + state = MSG_ARG; + i--; + break; + } + break; + case MSG_ARG: + switch (b) + { + case '\r': + break; + case '\n': + conn.processMsgArgs(argBufBase, argBufStream.Position); + argBufStream.Position = 0; + if (conn.msgArgs.size > msgBufBase.Length) + { + msgBufBase = new byte[conn.msgArgs.size+1]; + msgBufStream = new MemoryStream(msgBufBase); + } + state = MSG_PAYLOAD; + break; + default: + argBufStream.WriteByte((byte)b); + break; + } + break; + case MSG_PAYLOAD: + long position = msgBufStream.Position; + if (position >= conn.msgArgs.size) + { + conn.processMsg(msgBufBase, position); + msgBufStream.Position = 0; + state = MSG_END; + } + else + { + msgBufStream.WriteByte((byte)b); + } + break; + case MSG_END: + switch (b) + { + case '\n': + state = OP_START; + break; + default: + continue; + } + break; + case OP_PLUS: + switch (b) + { + case 'O': + case 'o': + state = OP_PLUS_O; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PLUS_O: + switch (b) + { + case 'K': + case 'k': + state = OP_PLUS_OK; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PLUS_OK: + switch (b) + { + case '\n': + conn.processOK(); + state = OP_START; + break; + } + break; + case OP_MINUS: + switch (b) + { + case 'E': + case 'e': + state = OP_MINUS_E; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MINUS_E: + switch (b) + { + case 'R': + case 'r': + state = OP_MINUS_ER; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MINUS_ER: + switch (b) + { + case 'R': + case 'r': + state = OP_MINUS_ERR; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MINUS_ERR: + switch (b) + { + case ' ': + case '\t': + state = OP_MINUS_ERR_SPC; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_MINUS_ERR_SPC: + switch (b) + { + case ' ': + case '\t': + state = OP_MINUS_ERR_SPC; + break; + default: + state = MINUS_ERR_ARG; + break; + } + break; + case MINUS_ERR_ARG: + switch (b) + { + case '\r': + break; + case '\n': + conn.processErr(argBufStream); + argBufStream.Position = 0; + state = OP_START; + break; + default: + argBufStream.WriteByte((byte)b); + break; + } + break; + case OP_P: + switch (b) + { + case 'I': + case 'i': + state = OP_PI; + break; + case 'O': + case 'o': + state = OP_PO; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PO: + switch (b) + { + case 'N': + case 'n': + state = OP_PON; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PON: + switch (b) + { + case 'G': + case 'g': + state = OP_PONG; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PONG: + switch (b) + { + case '\r': + break; + case '\n': + conn.processPong(); + state = OP_START; + break; + } + break; + case OP_PI: + switch (b) + { + case 'N': + case 'n': + state = OP_PIN; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PIN: + switch (b) + { + case 'G': + case 'g': + state = OP_PING; + break; + default: + parseError(buffer, i); + break; + } + break; + case OP_PING: + switch (b) + { + case '\r': + break; + case '\n': + conn.processPing(); + state = OP_START; + break; + default: + parseError(buffer, i); + break; + } + break; + default: + throw new NATSException("Unable to parse."); + } // switch(state) + + } // for + + } // parse + } +} \ No newline at end of file diff --git a/NATS/Properties/AssemblyInfo.cs b/NATS/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..dfffaa196 --- /dev/null +++ b/NATS/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NATS Client")] +[assembly: AssemblyDescription("NATS Client API")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apcera, Inc.")] +[assembly: AssemblyProduct("NATS")] +[assembly: AssemblyCopyright("Copyright © Apcera 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f9b43ebc-1cea-402c-9ff3-d9315f18e599")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.1.1.1")] +[assembly: AssemblyFileVersion("0.1.1.1")] diff --git a/NATS/Srv.cs b/NATS/Srv.cs new file mode 100755 index 000000000..d1d5eded9 --- /dev/null +++ b/NATS/Srv.cs @@ -0,0 +1,37 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; + +namespace NATS.Client +{ + // Tracks individual backend servers. + internal class Srv + { + internal Uri url = null; + internal bool didConnect = false; + internal int reconnects = 0; + internal System.DateTime lastAttempt = System.DateTime.Now; + + // never create a srv object without a url. + private Srv() { } + + internal Srv(String urlString) + { + this.url = new Uri(urlString); + } + + internal void updateLastAttempt() + { + lastAttempt = System.DateTime.Now; + } + + internal TimeSpan TimeSinceLastAttempt + { + get + { + return (DateTime.Now - lastAttempt); + } + } + } +} + diff --git a/NATS/Statistics.cs b/NATS/Statistics.cs new file mode 100755 index 000000000..21f0fa439 --- /dev/null +++ b/NATS/Statistics.cs @@ -0,0 +1,85 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; + +// disable XML comment warnings +#pragma warning disable 1591 + +namespace NATS.Client +{ + + public class Statistics : IStatistics + { + internal Statistics() { } + + internal long inMsgs = 0; + + /// + /// Gets the number of inbound messages received. + /// + public long InMsgs + { + get { return inMsgs; } + } + + internal long outMsgs = 0; + + /// + /// Gets the number of messages sent. + /// + public long OutMsgs + { + get { return outMsgs; } + } + + internal long inBytes = 0; + + /// + /// Gets the number of incoming bytes. + /// + public long InBytes + { + get { return inBytes; } + } + + internal long outBytes = 0; + + /// + /// Gets the outgoing number of bytes. + /// + public long OutBytes + { + get { return outBytes; } + } + + internal long reconnects = 0; + + /// + /// Gets the number of reconnections. + /// + public long Reconnects + { + get { return reconnects; } + } + + // deep copy contructor + internal Statistics(Statistics obj) + { + this.inMsgs = obj.inMsgs; + this.inBytes = obj.inBytes; + this.outBytes = obj.outBytes; + this.outMsgs = obj.outMsgs; + this.reconnects = obj.reconnects; + } + + internal void clear() + { + this.inBytes = 0; + this.inMsgs = 0; + this.outBytes = 0; + this.outMsgs = 0; + } + } + + +} \ No newline at end of file diff --git a/NATS/Subscription.cs b/NATS/Subscription.cs new file mode 100755 index 000000000..c526fa4f6 --- /dev/null +++ b/NATS/Subscription.cs @@ -0,0 +1,199 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Text; + +// disable XML comment warnings +#pragma warning disable 1591 + +namespace NATS.Client +{ + public class Subscription : ISubscription, IDisposable + { + readonly internal Object mu = new Object(); // lock + + internal long sid = 0; // subscriber ID. + private long msgs; + internal protected long delivered; + private long bytes; + internal protected long max = -1; + + // slow consumer + internal bool sc = false; + + internal Connection conn = null; + internal Channel mch = new Channel(); + + // Subject that represents this subscription. This can be different + // than the received subject inside a Msg if this is a wildcard. + private string subject = null; + + internal Subscription(Connection conn, string subject, string queue) + { + this.conn = conn; + this.subject = subject; + this.queue = queue; + } + + internal void closeChannel() + { + lock (mu) + { + mch.close(); + mch = null; + } + } + + public string Subject + { + get { return subject; } + } + + // Optional queue group name. If present, all subscriptions with the + // same name will form a distributed queue, and each message will + // only be processed by one member of the group. + string queue; + + public string Queue + { + get { return queue; } + } + + public Connection Connection + { + get + { + return conn; + } + } + + internal bool tallyMessage(long bytes) + { + lock (mu) + { + if (max > 0 && msgs > max) + return true; + + this.msgs++; + this.bytes += bytes; + + } + + return false; + } + + + protected internal virtual bool processMsg(Msg msg) + { + return true; + } + + // returns false if the message could not be added because + // the channel is full, true if the message was added + // to the channel. + internal bool addMessage(Msg msg, int maxCount) + { + if (mch != null) + { + if (mch.Count >= maxCount) + { + return false; + } + else + { + sc = false; + mch.add(msg); + } + } + return true; + } + + public bool IsValid + { + get + { + lock (mu) + { + return (conn != null); + } + } + } + + public virtual void Unsubscribe() + { + Connection c; + lock (mu) + { + c = this.conn; + } + + if (c == null) + throw new NATSBadSubscriptionException(); + + c.unsubscribe(this, 0); + } + + public virtual void AutoUnsubscribe(int max) + { + Connection c = null; + + lock (mu) + { + if (conn == null) + throw new NATSBadSubscriptionException(); + + c = conn; + } + + c.unsubscribe(this, max); + } + + public int QueuedMessageCount + { + get + { + lock (mu) + { + if (this.conn == null) + throw new NATSBadSubscriptionException(); + + return mch.Count; + } + } + } + + void IDisposable.Dispose() + { + try + { + Unsubscribe(); + } + catch (Exception) + { + // We we get here with normal usage, for example when + // auto unsubscribing, so just this here. + } + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + sb.Append("{"); + + sb.AppendFormat("Subject={0};Queue={1};" + + "QueuedMessageCount={2};IsValid={3};Type={4}", + Subject, (Queue == null ? "null" : Queue), + QueuedMessageCount, IsValid, + this.GetType().ToString()); + + sb.Append("}"); + + return sb.ToString(); + } + + } // Subscription + +} \ No newline at end of file diff --git a/NATS/SyncSub.cs b/NATS/SyncSub.cs new file mode 100755 index 000000000..2e2147e66 --- /dev/null +++ b/NATS/SyncSub.cs @@ -0,0 +1,69 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +// disable XM comment warnings +#pragma warning disable 1591 + +namespace NATS.Client +{ + public sealed class SyncSubscription : Subscription, ISyncSubscription, ISubscription + { + internal SyncSubscription(Connection conn, string subject, string queue) + : base(conn, subject, queue) { } + + public Msg NextMessage() + { + return NextMessage(-1); + } + + public Msg NextMessage(int timeout) + { + Connection localConn; + Channel localChannel; + long localMax; + Msg msg; + + lock (mu) + { + if (conn == null) + throw new NATSBadSubscriptionException(); + if (mch == null) + throw new NATSConnectionClosedException(); + if (sc) + throw new NATSSlowConsumerException(); + + localConn = this.conn; + localChannel = this.mch; + localMax = this.max; + } + + if (timeout >= 0) + { + msg = localChannel.get(timeout); + } + else + { + msg = localChannel.get(-1); + } + + if (msg != null) + { + long d = Interlocked.Increment(ref this.delivered); + if (d == max) + { + // Remove subscription if we have reached max. + localConn.removeSub(this); + } + if (localMax > 0 && d > localMax) + { + throw new NATSException("nats: Max messages delivered"); + } + } + + return msg; + } + } +} \ No newline at end of file diff --git a/NATSUnitTests/NATSUnitTests.csproj b/NATSUnitTests/NATSUnitTests.csproj new file mode 100755 index 000000000..27bfb8ea9 --- /dev/null +++ b/NATSUnitTests/NATSUnitTests.csproj @@ -0,0 +1,113 @@ + + + + Debug + AnyCPU + {00DBFD4D-72F4-4250-884C-C1527C66A0C2} + Library + Properties + NATSUnitTests + NATSUnitTests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + + + + + + + + + + {68ee71d4-9532-470e-b5ca-ecaa79936b1f} + NATS + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + False + + + False + + + False + + + False + + + + + + + + diff --git a/NATSUnitTests/Properties/AssemblyInfo.cs b/NATSUnitTests/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..e787475fd --- /dev/null +++ b/NATSUnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NATSUnitTests")] +[assembly: AssemblyDescription("NATS Unit Tests")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apcera, Inc.")] +[assembly: AssemblyProduct("NATSUnitTests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("07839bfb-b7e8-4b99-b9a3-5f18e29adef3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/NATSUnitTests/Properties/Settings.Designer.cs b/NATSUnitTests/Properties/Settings.Designer.cs new file mode 100755 index 000000000..7933520a4 --- /dev/null +++ b/NATSUnitTests/Properties/Settings.Designer.cs @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace NATSUnitTests.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("gnatsd.exe")] + public string gnatsd { + get { + return ((string)(this["gnatsd"])); + } + } + } +} diff --git a/NATSUnitTests/Properties/Settings.settings b/NATSUnitTests/Properties/Settings.settings new file mode 100755 index 000000000..8c767a591 --- /dev/null +++ b/NATSUnitTests/Properties/Settings.settings @@ -0,0 +1,9 @@ + + + + + + C:\Go\bin\gnatsd.exe + + + \ No newline at end of file diff --git a/NATSUnitTests/Settings.cs b/NATSUnitTests/Settings.cs new file mode 100755 index 000000000..de1ef8d46 --- /dev/null +++ b/NATSUnitTests/Settings.cs @@ -0,0 +1,31 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +namespace NATSUnitTests.Properties +{ + + + // This class allows you to handle specific events on the settings class: + // The SettingChanging event is raised before a setting's value is changed. + // The PropertyChanged event is raised after a setting's value is changed. + // The SettingsLoaded event is raised after the setting values are loaded. + // The SettingsSaving event is raised before the setting values are saved. + internal sealed partial class Settings { + + public Settings() { + // // To add event handlers for saving and changing settings, uncomment the lines below: + // + // this.SettingChanging += this.SettingChangingEventHandler; + // + // this.SettingsSaving += this.SettingsSavingEventHandler; + // + } + + private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { + // Add code to handle the SettingChangingEvent event here. + } + + private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { + // Add code to handle the SettingsSaving event here. + } + } +} diff --git a/NATSUnitTests/UnitTestAuth.cs b/NATSUnitTests/UnitTestAuth.cs new file mode 100755 index 000000000..82fff78f3 --- /dev/null +++ b/NATSUnitTests/UnitTestAuth.cs @@ -0,0 +1,90 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NATS.Client; + +namespace NATSUnitTests +{ + /// + /// Run these tests with the gnatsd auth.conf configuration file. + /// + [TestClass] + public class TestAuthorization + { + int hitDisconnect; + + UnitTestUtilities util = new UnitTestUtilities(); + + private void connectAndFail(String url) + { + try + { + System.Console.WriteLine("Trying: " + url); + + hitDisconnect = 0; + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = url; + opts.DisconnectedEventHandler += handleDisconnect; + IConnection c = new ConnectionFactory().CreateConnection(url); + + Assert.Fail("Expected a failure; did not receive one"); + + c.Close(); + } + catch (Exception e) + { + if (e.Message.Contains("Authorization")) + { + System.Console.WriteLine("Success with expected failure: " + e.Message); + } + else + { + Assert.Fail("Unexpected exception thrown: " + e); + } + } + finally + { + if (hitDisconnect > 0) + Assert.Fail("The disconnect event handler was incorrectly invoked."); + } + } + + private void handleDisconnect(object sender, ConnEventArgs e) + { + hitDisconnect++; + } + + [TestMethod] + public void TestAuthSuccess() + { + using (NATSServer s = util.CreateServerWithConfig("auth_1222.conf")) + { + IConnection c = new ConnectionFactory().CreateConnection("nats://username:password@localhost:1222"); + c.Close(); + } + } + + [TestMethod] + public void TestAuthFailure() + { + try + { + using (NATSServer s = util.CreateServerWithConfig("auth_1222.conf")) + { + connectAndFail("nats://username@localhost:1222"); + connectAndFail("nats://username:badpass@localhost:1222"); + connectAndFail("nats://localhost:1222"); + connectAndFail("nats://badname:password@localhost:1222"); + } + + + } + catch (Exception e) + { + System.Console.WriteLine(e); + throw e; + } + } + } +} diff --git a/NATSUnitTests/UnitTestBasic.cs b/NATSUnitTests/UnitTestBasic.cs new file mode 100755 index 000000000..532511612 --- /dev/null +++ b/NATSUnitTests/UnitTestBasic.cs @@ -0,0 +1,650 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NATS.Client; + +namespace NATSUnitTests +{ + /// + /// Run these tests with the gnatsd auth.conf configuration file. + /// + [TestClass] + public class TestBasic + { + UnitTestUtilities utils = new UnitTestUtilities(); + + [TestInitialize()] + public void Initialize() + { + utils.StartDefaultServer(); + } + + [TestCleanup()] + public void Cleanup() + { + utils.StopDefaultServer(); + } + + [TestMethod] + public void TestConnectedServer() + { + IConnection c = new ConnectionFactory().CreateConnection(); + + string u = c.ConnectedUrl; + + if (string.IsNullOrWhiteSpace(u)) + Assert.Fail("Invalid connected url {0}.", u); + + if (!Defaults.Url.Equals(u)) + Assert.Fail("Invalid connected url {0}.", u); + + c.Close(); + u = c.ConnectedUrl; + + if (u != null) + Assert.Fail("Url is not null after connection is closed."); + } + + [TestMethod] + public void TestMultipleClose() + { + IConnection c = new ConnectionFactory().CreateConnection(); + + Task[] tasks = new Task[10]; + + for (int i = 0; i < 10; i++) + { + + tasks[i] = new Task(() => { c.Close(); }); + tasks[i].Start(); + } + + Task.WaitAll(tasks); + } + + [TestMethod] + public void TestBadOptionTimeoutConnect() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + + try + { + opts.Timeout = -1; + Assert.Fail("Able to set invalid timeout."); + } + catch (Exception) + {} + } + + [TestMethod] + public void TestSimplePublish() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + c.Publish("foo", Encoding.UTF8.GetBytes("Hello World!")); + } + } + + [TestMethod] + public void TestSimplePublishNoData() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + c.Publish("foo", null); + } + } + + private bool compare(byte[] p1, byte[] p2) + { + // null case + if (p1 == p2) + return true; + + if (p1.Length != p2.Length) + return false; + + for (int i = 0; i < p2.Length; i++) + { + if (p1[i] != p2[i]) + return false; + } + + return true; + } + + private bool compare(byte[] payload, Msg m) + { + return compare(payload, m.Data); + } + + private bool compare(Msg a, Msg b) + { + if (a.Subject.Equals(b.Subject) == false) + return false; + + if (a.Reply != null && a.Reply.Equals(b.Reply)) + { + return false; + } + + return compare(a.Data, b.Data); + } + + readonly byte[] omsg = Encoding.UTF8.GetBytes("Hello World"); + readonly object mu = new Object(); + IAsyncSubscription asyncSub = null; + Boolean received = false; + + [TestMethod] + public void TestAsyncSubscribe() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + asyncSub = s; + s.MessageHandler += CheckRecveivedAndValidHandler; + s.Start(); + + lock (mu) + { + received = false; + c.Publish("foo", omsg); + c.Flush(); + Monitor.Wait(mu, 30000); + } + + if (!received) + Assert.Fail("Did not receive message."); + } + } + } + + private void CheckRecveivedAndValidHandler(object sender, MsgHandlerEventArgs args) + { + System.Console.WriteLine("Received msg."); + + if (compare(args.Message.Data, omsg) == false) + Assert.Fail("Messages are not equal."); + + if (args.Message.ArrivalSubcription != asyncSub) + Assert.Fail("Subscriptions do not match."); + + lock (mu) + { + received = true; + Monitor.Pulse(mu); + } + } + + [TestMethod] + public void TestSyncSubscribe() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + c.Publish("foo", omsg); + Msg m = s.NextMessage(1000); + if (compare(omsg, m) == false) + Assert.Fail("Messages are not equal."); + } + } + } + + [TestMethod] + public void TestPubWithReply() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + c.Publish("foo", "reply", omsg); + Msg m = s.NextMessage(1000); + if (compare(omsg, m) == false) + Assert.Fail("Messages are not equal."); + } + } + } + + [TestMethod] + public void TestFlush() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + c.Publish("foo", "reply", omsg); + c.Flush(); + } + } + } + + [TestMethod] + public void TestQueueSubscriber() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s1 = c.SubscribeSync("foo", "bar"), + s2 = c.SubscribeSync("foo", "bar")) + { + c.Publish("foo", omsg); + c.Flush(1000); + + if (s1.QueuedMessageCount + s2.QueuedMessageCount != 1) + Assert.Fail("Invalid message count in queue."); + + // Drain the messages. + try { s1.NextMessage(100); } + catch (NATSTimeoutException) { } + + try { s2.NextMessage(100); } + catch (NATSTimeoutException) { } + + int total = 1000; + + for (int i = 0; i < 1000; i++) + { + c.Publish("foo", omsg); + } + c.Flush(1000); + + Thread.Sleep(1000); + + int r1 = s1.QueuedMessageCount; + int r2 = s2.QueuedMessageCount; + + if ((r1 + r2) != total) + { + Assert.Fail("Incorrect number of messages: {0} vs {1}", + (r1 + r2), total); + } + + if (Math.Abs(r1 - r2) > (total * .15)) + { + Assert.Fail("Too much variance between {0} and {1}", + r1, r2); + } + } + } + } + + [TestMethod] + public void TestReplyArg() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + s.MessageHandler += ExpectedReplyHandler; + s.Start(); + + lock(mu) + { + received = false; + c.Publish("foo", "bar", null); + Monitor.Wait(mu, 5000); + } + } + } + + if (!received) + Assert.Fail("Message not received."); + } + + private void ExpectedReplyHandler(object sender, MsgHandlerEventArgs args) + { + if ("bar".Equals(args.Message.Reply) == false) + Assert.Fail("Expected \"bar\", received: " + args.Message); + + lock(mu) + { + received = true; + Monitor.Pulse(mu); + } + } + + [TestMethod] + public void TestSyncReplyArg() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + c.Publish("foo", "bar", null); + c.Flush(30000); + + Msg m = s.NextMessage(1000); + if ("bar".Equals(m.Reply) == false) + Assert.Fail("Expected \"bar\", received: " + m); + } + } + } + + [TestMethod] + public void TestUnsubscribe() + { + int count = 0; + int max = 20; + + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + Boolean unsubscribed = false; + asyncSub = s; + //s.MessageHandler += UnsubscribeAfterCount; + s.MessageHandler += (sender, args) => + { + count++; + System.Console.WriteLine("Count = {0}", count); + if (count == max) + { + asyncSub.Unsubscribe(); + lock (mu) + { + unsubscribed = true; + Monitor.Pulse(mu); + } + } + }; + s.Start(); + + max = 20; + for (int i = 0; i < max; i++) + { + c.Publish("foo", null, null); + } + Thread.Sleep(100); + c.Flush(); + + lock (mu) + { + if (!unsubscribed) + { + Monitor.Wait(mu, 5000); + } + } + } + + if (count != max) + Assert.Fail("Received wrong # of messages after unsubscribe: {0} vs {1}", count, max); + } + } + + [TestMethod] + public void TestDoubleUnsubscribe() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + s.Unsubscribe(); + + try + { + s.Unsubscribe(); + Assert.Fail("No Exception thrown."); + } + catch (Exception e) + { + System.Console.WriteLine("Expected exception {0}: {1}", + e.GetType(), e.Message); + } + } + } + } + + [TestMethod] + public void TestRequestTimeout() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + try + { + c.Request("foo", null, 500); + Assert.Fail("Expected an exception."); + } + catch (NATSTimeoutException) + { + Console.WriteLine("Received expected exception."); + } + } + } + + [TestMethod] + public void TestRequest() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + byte[] response = Encoding.UTF8.GetBytes("I will help you."); + + s.MessageHandler += (sender, args) => + { + c.Publish(args.Message.Reply, response); + c.Flush(); + }; + + s.Start(); + + Msg m = c.Request("foo", Encoding.UTF8.GetBytes("help."), + 5000); + + if (!compare(m.Data, response)) + { + Assert.Fail("Response isn't valid"); + } + } + } + } + + [TestMethod] + public void TestRequestNoBody() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + byte[] response = Encoding.UTF8.GetBytes("I will help you."); + + s.MessageHandler += (sender, args) => + { + c.Publish(args.Message.Reply, response); + }; + + s.Start(); + + Msg m = c.Request("foo", null, 50000); + + if (!compare(m.Data, response)) + { + Assert.Fail("Response isn't valid"); + } + } + } + } + + [TestMethod] + public void TestFlushInHandler() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + byte[] response = Encoding.UTF8.GetBytes("I will help you."); + + s.MessageHandler += (sender, args) => + { + try + { + c.Flush(); + System.Console.WriteLine("Success."); + } + catch (Exception e) + { + Assert.Fail("Unexpected exception: " + e); + } + + lock (mu) + { + Monitor.Pulse(mu); + } + }; + + s.Start(); + + lock (mu) + { + c.Publish("foo", Encoding.UTF8.GetBytes("Hello")); + Monitor.Wait(mu); + } + } + } + } + + [TestMethod] + public void TestReleaseFlush() + { + IConnection c = new ConnectionFactory().CreateConnection(); + + for (int i = 0; i < 1000; i++) + { + c.Publish("foo", Encoding.UTF8.GetBytes("Hello")); + } + + new Task(() => { c.Close(); }).Start(); + c.Flush(); + } + + [TestMethod] + public void TestCloseAndDispose() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + c.Close(); + } + } + + [TestMethod] + public void TestInbox() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + string inbox = c.NewInbox(); + Assert.IsFalse(string.IsNullOrWhiteSpace(inbox)); + Assert.IsTrue(inbox.StartsWith("_INBOX.")); + } + } + + [TestMethod] + public void TestStats() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + byte[] data = Encoding.UTF8.GetBytes("The quick brown fox jumped over the lazy dog"); + int iter = 10; + + for (int i = 0; i < iter; i++) + { + c.Publish("foo", data); + } + c.Flush(1000); + + IStatistics stats = c.Stats; + Assert.AreEqual(iter, stats.OutMsgs); + Assert.AreEqual(iter * data.Length, stats.OutBytes); + + c.ResetStats(); + + // Test both sync and async versions of subscribe. + IAsyncSubscription s1 = c.SubscribeAsync("foo"); + s1.MessageHandler += (sender, arg) => { }; + s1.Start(); + + ISyncSubscription s2 = c.SubscribeSync("foo"); + + for (int i = 0; i < iter; i++) + { + c.Publish("foo", data); + } + c.Flush(1000); + + stats = c.Stats; + Assert.AreEqual(2 * iter, stats.InMsgs); + Assert.AreEqual(2 * iter * data.Length, stats.InBytes); + } + } + + [TestMethod] + public void TestRaceSafeStats() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + + new Task(() => { c.Publish("foo", null); }).Start(); + + Thread.Sleep(1000); + + Assert.AreEqual(1, c.Stats.OutMsgs); + } + } + + [TestMethod] + public void TestBadSubject() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + new Task(() => { c.Publish("foo", null); }).Start(); + Thread.Sleep(200); + + Assert.AreEqual(1, c.Stats.OutMsgs); + } + } + + [TestMethod] + public void TestLargeMessage() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + int msgSize = 20480; + byte[] msg = new byte[msgSize]; + + for (int i = 0; i < msgSize; i++) + msg[i] = (byte)'A'; + + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + Object testLock = new Object(); + + s.MessageHandler += (sender, args) => + { + lock(testLock) + { + Monitor.Pulse(testLock); + } + Assert.IsTrue(compare(msg, args.Message.Data)); + }; + + s.Start(); + + c.Publish("foo", msg); + c.Flush(2000); + + lock(testLock) + { + Monitor.Wait(testLock, 2000); + } + } + } + } + + + } // class + +} // namespace diff --git a/NATSUnitTests/UnitTestCluster.cs b/NATSUnitTests/UnitTestCluster.cs new file mode 100755 index 000000000..3074313b9 --- /dev/null +++ b/NATSUnitTests/UnitTestCluster.cs @@ -0,0 +1,508 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NATS.Client; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace NATSUnitTests +{ + /// + /// Run these tests with the gnatsd auth.conf configuration file. + /// + [TestClass] + public class TestCluster + { + string[] testServers = new string[] { + "nats://localhost:1222", + "nats://localhost:1223", + "nats://localhost:1224", + "nats://localhost:1225", + "nats://localhost:1226", + "nats://localhost:1227", + "nats://localhost:1228" + }; + + string[] testServersShortList = new string[] { + "nats://localhost:1222", + "nats://localhost:1223" + }; + + UnitTestUtilities utils = new UnitTestUtilities(); + + [TestMethod] + public void TestServersOption() + { + IConnection c = null; + ConnectionFactory cf = new ConnectionFactory(); + Options o = ConnectionFactory.GetDefaultOptions(); + + o.NoRandomize = true; + + UnitTestUtilities.testExpectedException( + () => { cf.CreateConnection(); }, + typeof(NATSNoServersException)); + + o.Servers = testServers; + + UnitTestUtilities.testExpectedException( + () => { cf.CreateConnection(o); }, + typeof(NATSNoServersException)); + + // Make sure we can connect to first server if running + using (NATSServer ns = utils.CreateServerOnPort(1222)) + { + c = cf.CreateConnection(o); + Assert.IsTrue(testServers[0].Equals(c.ConnectedUrl)); + c.Close(); + } + + // make sure we can connect to a non-first server. + using (NATSServer ns = utils.CreateServerOnPort(1227)) + { + c = cf.CreateConnection(o); + Assert.IsTrue(testServers[5].Equals(c.ConnectedUrl)); + c.Close(); + } + } + + [TestMethod] + public void TestAuthServers() + { + string[] plainServers = new string[] { + "nats://localhost:1222", + "nats://localhost:1224" + }; + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.NoRandomize = true; + opts.Servers = plainServers; + opts.Timeout = 5000; + + using (NATSServer as1 = utils.CreateServerWithConfig("auth_1222.conf"), + as2 = utils.CreateServerWithConfig("auth_1224.conf")) + { + UnitTestUtilities.testExpectedException( + () => { new ConnectionFactory().CreateConnection(opts); }, + typeof(NATSException)); + + // Test that we can connect to a subsequent correct server. + string[] authServers = new string[] { + "nats://localhost:1222", + "nats://username:password@localhost:1224"}; + + opts.Servers = authServers; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + Assert.IsTrue(c.ConnectedUrl.Equals(authServers[1])); + } + } + } + + [TestMethod] + public void TestBasicClusterReconnect() + { + string[] plainServers = new string[] { + "nats://localhost:1222", + "nats://localhost:1224" + }; + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.MaxReconnect = 2; + opts.ReconnectWait = 1000; + opts.NoRandomize = true; + opts.Servers = plainServers; + + Object disconnectLock = new Object(); + opts.DisconnectedEventHandler += (sender, args) => + { + // Suppress any additional calls + opts.DisconnectedEventHandler = null; + lock (disconnectLock) + { + Monitor.Pulse(disconnectLock); + } + }; + + Object reconnectLock = new Object(); + + opts.ReconnectedEventHandler = (sender, args) => + { + // Suppress any additional calls + lock (reconnectLock) + { + Monitor.Pulse(reconnectLock); + } + }; + + opts.Timeout = 200; + + using (NATSServer s1 = utils.CreateServerOnPort(1222), + s2 = utils.CreateServerOnPort(1224)) + { + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + Stopwatch reconnectSw = new Stopwatch(); + + System.Console.WriteLine("Connected to: " + c.ConnectedUrl); + lock (disconnectLock) + { + s1.Shutdown(); + Assert.IsTrue(Monitor.Wait(disconnectLock, 20000)); + } + + reconnectSw.Start(); + + lock (reconnectLock) + { + Assert.IsTrue(Monitor.Wait(reconnectLock, 20000)); + } + + Assert.IsTrue(c.ConnectedUrl.Equals(testServers[2])); + + reconnectSw.Stop(); + + // Make sure we did not wait on reconnect for default time. + // Reconnect should be fast since it will be a switch to the + // second server and not be dependent on server restart time. + // TODO: .NET connect timeout is exceeding long compared to + // GO's. Look shortening it, or living with it. + //if (reconnectSw.ElapsedMilliseconds > opts.ReconnectWait) + //{ + // Assert.Fail("Reconnect time took to long: {0} millis.", + // reconnectSw.ElapsedMilliseconds); + //} + } + } + } + + private class SimClient + { + IConnection c; + Object mu = new Object(); + + public string ConnectedUrl + { + get { return c.ConnectedUrl; } + } + + public void waitForReconnect() + { + lock (mu) + { + Monitor.Wait(mu); + } + } + + public void Connect(string[] servers) + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Servers = servers; + c = new ConnectionFactory().CreateConnection(opts); + opts.ReconnectedEventHandler = (sender, args) => + { + lock (mu) + { + Monitor.Pulse(mu); + } + }; + } + + public void close() + { + c.Close(); + } + } + + + //[TestMethod] + public void TestHotSpotReconnect() + { + int numClients = 10; + SimClient[] clients = new SimClient[100]; + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Servers = testServers; + + NATSServer s1 = utils.CreateServerOnPort(1222); + Task[] waitgroup = new Task[numClients]; + + + for (int i = 0; i < numClients; i++) + { + clients[i] = new SimClient(); + Task t = new Task(() => { + clients[i].Connect(testServers); + clients[i].waitForReconnect(); + }); + t.Start(); + waitgroup[i] = t; + } + + + NATSServer s2 = utils.CreateServerOnPort(1224); + NATSServer s3 = utils.CreateServerOnPort(1226); + + s1.Shutdown(); + Task.WaitAll(waitgroup); + + int s2Count = 0; + int s3Count = 0; + int unknown = 0; + + for (int i = 0; i < numClients; i++) + { + if (testServers[3].Equals(clients[i].ConnectedUrl)) + s2Count++; + else if (testServers[5].Equals(clients[i].ConnectedUrl)) + s3Count++; + else + unknown++; + } + + Assert.IsTrue(unknown == 0); + int delta = Math.Abs(s2Count - s3Count); + int range = numClients / 30; + if (delta > range) + { + Assert.Fail("Connected clients to servers out of range: {0}/{1}", delta, range); + } + + } + + [TestMethod] + public void TestProperReconnectDelay() + { + Object mu = new Object(); + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Servers = testServers; + opts.NoRandomize = true; + + bool disconnectHandlerCalled = false; + opts.DisconnectedEventHandler = (sender, args) => + { + opts.DisconnectedEventHandler = null; + disconnectHandlerCalled = true; + lock (mu) + { + disconnectHandlerCalled = true; + Monitor.Pulse(mu); + } + }; + + bool closedCbCalled = false; + opts.ClosedEventHandler = (sender, args) => + { + closedCbCalled = true; + }; + + using (NATSServer s1 = utils.CreateServerOnPort(1222)) + { + IConnection c = new ConnectionFactory().CreateConnection(opts); + + lock (mu) + { + s1.Shutdown(); + // wait for disconnect + Assert.IsTrue(Monitor.Wait(mu, 10000)); + + + // Wait, want to make sure we don't spin on + //reconnect to non-existant servers. + Thread.Sleep(1000); + + Assert.IsFalse(closedCbCalled); + Assert.IsTrue(disconnectHandlerCalled); + Assert.IsTrue(c.State == ConnState.RECONNECTING); + } + + } + } + + [TestMethod] + public void TestProperFalloutAfterMaxAttempts() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + + Object dmu = new Object(); + Object cmu = new Object(); + + opts.Servers = this.testServersShortList; + opts.NoRandomize = true; + opts.MaxReconnect = 2; + opts.ReconnectWait = 25; // millis + opts.Timeout = 500; + + bool disconnectHandlerCalled = false; + + opts.DisconnectedEventHandler = (sender, args) => + { + lock (dmu) + { + disconnectHandlerCalled = true; + Monitor.Pulse(dmu); + } + }; + + bool closedHandlerCalled = false; + opts.ClosedEventHandler = (sender, args) => + { + lock (cmu) + { + closedHandlerCalled = true; + Monitor.Pulse(cmu); + } + }; + + using (NATSServer s1 = utils.CreateServerOnPort(1222)) + { + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + s1.Shutdown(); + + lock (dmu) + { + if (!disconnectHandlerCalled) + Assert.IsTrue(Monitor.Wait(dmu, 20000)); + } + + lock (cmu) + { + if (!closedHandlerCalled) + Assert.IsTrue(Monitor.Wait(cmu, 60000)); + } + + Assert.IsTrue(disconnectHandlerCalled); + Assert.IsTrue(closedHandlerCalled); + Assert.IsTrue(c.IsClosed()); + } + } + } + + [TestMethod] + public void TestTimeoutOnNoServers() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + Object dmu = new Object(); + Object cmu = new Object(); + + opts.Servers = testServersShortList; + opts.NoRandomize = true; + opts.MaxReconnect = 2; + opts.ReconnectWait = 100; // millis + + bool disconnectHandlerCalled = false; + bool closedHandlerCalled = false; + + opts.DisconnectedEventHandler = (sender, args) => + { + lock (dmu) + { + disconnectHandlerCalled = true; + Monitor.Pulse(dmu); + } + }; + + opts.ClosedEventHandler = (sender, args) => + { + lock (cmu) + { + closedHandlerCalled = true; + Monitor.Pulse(cmu); + } + }; + + using (NATSServer s1 = utils.CreateServerOnPort(1222)) + { + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + s1.Shutdown(); + + lock (dmu) + { + if (!disconnectHandlerCalled) + Assert.IsTrue(Monitor.Wait(dmu, 20000)); + } + + Stopwatch sw = new Stopwatch(); + sw.Start(); + + lock (cmu) + { + if (!closedHandlerCalled) + Assert.IsTrue(Monitor.Wait(cmu, 60000)); + } + + sw.Stop(); + + int expected = opts.MaxReconnect * opts.ReconnectWait; + + // .NET has long connect times, so revisit this after + // a connect timeout has been added. + //Assert.IsTrue(sw.ElapsedMilliseconds < (expected + 500)); + + Assert.IsTrue(disconnectHandlerCalled); + Assert.IsTrue(closedHandlerCalled); + Assert.IsTrue(c.IsClosed()); + } + } + } + + //[TestMethod] + public void TestPingReconnect() + { + /// Work in progress + int RECONNECTS = 4; + + Options opts = ConnectionFactory.GetDefaultOptions(); + Object mu = new Object(); + + opts.Servers = testServersShortList; + opts.NoRandomize = true; + opts.ReconnectWait = 200; + opts.PingInterval = 50; + opts.MaxPingsOut = -1; + opts.Timeout = 1000; + + + Stopwatch disconnectedTimer = new Stopwatch(); + + opts.DisconnectedEventHandler = (sender, args) => + { + disconnectedTimer.Reset(); + disconnectedTimer.Start(); + }; + + opts.ReconnectedEventHandler = (sender, args) => + { + lock (mu) + { + args.Conn.Opts.MaxPingsOut = 500; + disconnectedTimer.Stop(); + Monitor.Pulse(mu); + } + }; + + using (NATSServer s1 = utils.CreateServerOnPort(1222)) + { + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + s1.Shutdown(); + for (int i = 0; i < RECONNECTS; i++) + { + lock (mu) + { + Assert.IsTrue(Monitor.Wait(mu, 100000)); + } + } + } + } + } + + } // class + +} // namespace + diff --git a/NATSUnitTests/UnitTestConn.cs b/NATSUnitTests/UnitTestConn.cs new file mode 100755 index 000000000..a3ae5d8f2 --- /dev/null +++ b/NATSUnitTests/UnitTestConn.cs @@ -0,0 +1,180 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NATS.Client; +using System.Threading; + +namespace NATSUnitTests +{ + /// + /// Run these tests with the gnatsd auth.conf configuration file. + /// + [TestClass] + public class TestConnection + { + + UnitTestUtilities utils = new UnitTestUtilities(); + + [TestInitialize()] + public void Initialize() + { + utils.StartDefaultServer(); + } + + [TestCleanup()] + public void Cleanup() + { + utils.StopDefaultServer(); + } + + [TestMethod] + public void TestConnectionStatus() + { + IConnection c = new ConnectionFactory().CreateConnection(); + Assert.AreEqual(ConnState.CONNECTED, c.State); + c.Close(); + Assert.AreEqual(ConnState.CLOSED, c.State); + } + + [TestMethod] + public void TestCloseHandler() + { + bool closed = false; + + Options o = ConnectionFactory.GetDefaultOptions(); + o.ClosedEventHandler += (sender, args) => { closed = true; }; + IConnection c = new ConnectionFactory().CreateConnection(o); + c.Close(); + Assert.IsTrue(closed); + + // now test using. + closed = false; + using (c = new ConnectionFactory().CreateConnection(o)) { }; + Assert.IsTrue(closed); + } + + [TestMethod] + public void TestCloseDisconnectedHandler() + { + bool disconnected = false; + Object mu = new Object(); + + Options o = ConnectionFactory.GetDefaultOptions(); + o.AllowReconnect = false; + o.DisconnectedEventHandler += (sender, args) => { + lock (mu) + { + disconnected = true; + Monitor.Pulse(mu); + } + }; + + IConnection c = new ConnectionFactory().CreateConnection(o); + lock (mu) + { + c.Close(); + Monitor.Wait(mu, 20000); + } + Assert.IsTrue(disconnected); + + // now test using. + disconnected = false; + lock (mu) + { + using (c = new ConnectionFactory().CreateConnection(o)) { }; + Monitor.Wait(mu); + } + Assert.IsTrue(disconnected); + } + + [TestMethod] + public void TestServerStopDisconnectedHandler() + { + bool disconnected = false; + Object mu = new Object(); + + Options o = ConnectionFactory.GetDefaultOptions(); + o.AllowReconnect = false; + o.DisconnectedEventHandler += (sender, args) => + { + lock (mu) + { + disconnected = true; + Monitor.Pulse(mu); + } + }; + + IConnection c = new ConnectionFactory().CreateConnection(o); + lock (mu) + { + utils.bounceDefaultServer(1000); + Monitor.Wait(mu); + } + c.Close(); + Assert.IsTrue(disconnected); + } + + [TestMethod] + public void TestClosedConnections() + { + IConnection c = new ConnectionFactory().CreateConnection(); + ISyncSubscription s = c.SubscribeSync("foo"); + + c.Close(); + + // While we can annotate all the exceptions in the test framework, + // just do it manually. + UnitTestUtilities.testExpectedException( + () => { c.Publish("foo", null); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { c.Publish(new Msg("foo")); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { c.SubscribeAsync("foo"); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { c.SubscribeSync("foo"); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { c.SubscribeAsync("foo", "bar"); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { c.SubscribeSync("foo", "bar"); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { c.Request("foo", null); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { s.NextMessage(); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { s.NextMessage(100); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { s.Unsubscribe(); }, + typeof(NATSConnectionClosedException)); + + UnitTestUtilities.testExpectedException( + () => { s.AutoUnsubscribe(1); }, + typeof(NATSConnectionClosedException)); + } + + /// NOT IMPLEMENTED: + /// TestServerSecureConnections + /// TestErrOnConnectAndDeadlock + /// TestErrOnMaxPayloadLimit + + } // class + +} // namespace diff --git a/NATSUnitTests/UnitTestReconnect.cs b/NATSUnitTests/UnitTestReconnect.cs new file mode 100755 index 000000000..29af2a5b7 --- /dev/null +++ b/NATSUnitTests/UnitTestReconnect.cs @@ -0,0 +1,495 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NATS.Client; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace NATSUnitTests +{ + /// + /// Run these tests with the gnatsd auth.conf configuration file. + /// + [TestClass] + public class TestReconnect + { + + private Options reconnectOptions = getReconnectOptions(); + + private static Options getReconnectOptions() + { + Options o = ConnectionFactory.GetDefaultOptions(); + o.Url = "nats://localhost:22222"; + o.AllowReconnect = true; + o.MaxReconnect = 10; + o.ReconnectWait = 100; + + return o; + } + + UnitTestUtilities utils = new UnitTestUtilities(); + + [TestInitialize()] + public void Initialize() + { + // utils.StartDefaultServer(); + } + + [TestCleanup()] + public void Cleanup() + { + // utils.StopDefaultServer(); + } + + [TestMethod] + public void TestReconnectDisallowedFlags() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = "nats://localhost:22222"; + opts.AllowReconnect = false; + + Object testLock = new Object(); + + opts.ClosedEventHandler = (sender, args) => + { + lock(testLock) + { + Monitor.Pulse(testLock); + } + }; + + using (NATSServer ns = utils.CreateServerOnPort(22222)) + { + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + lock (testLock) + { + ns.Shutdown(); + Assert.IsTrue(Monitor.Wait(testLock, 1000)); + } + } + } + } + + [TestMethod] + public void TestReconnectAllowedFlags() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = "nats://localhost:22222"; + opts.MaxReconnect = 2; + opts.ReconnectWait = 1000; + + Object testLock = new Object(); + + opts.ClosedEventHandler = (sender, args) => + { + lock (testLock) + { + Monitor.Pulse(testLock); + } + }; + + using (NATSServer ns = utils.CreateServerOnPort(22222)) + { + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + lock (testLock) + { + ns.Shutdown(); + Assert.IsFalse(Monitor.Wait(testLock, 1000)); + } + + Assert.IsTrue(c.State == ConnState.RECONNECTING); + c.Opts.ClosedEventHandler = null; + } + } + } + + [TestMethod] + public void TestBasicReconnectFunctionality() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = "nats://localhost:22222"; + opts.MaxReconnect = 2; + opts.ReconnectWait = 1000; + + Object testLock = new Object(); + Object msgLock = new Object(); + + opts.DisconnectedEventHandler = (sender, args) => + { + lock (testLock) + { + Monitor.Pulse(testLock); + } + }; + + opts.ReconnectedEventHandler = (sender, args) => + { + System.Console.WriteLine("Reconnected"); + }; + + NATSServer ns = utils.CreateServerOnPort(22222); + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + IAsyncSubscription s = c.SubscribeAsync("foo"); + s.MessageHandler += (sender, args) => + { + System.Console.WriteLine("Received message."); + lock (msgLock) + { + Monitor.Pulse(msgLock); + } + }; + + s.Start(); + c.Flush(); + + lock (testLock) + { + ns.Shutdown(); + Assert.IsTrue(Monitor.Wait(testLock, 100000)); + } + + System.Console.WriteLine("Sending message."); + c.Publish("foo", Encoding.UTF8.GetBytes("Hello")); + System.Console.WriteLine("Done sending message."); + // restart the server. + using (ns = utils.CreateServerOnPort(22222)) + { + lock (msgLock) + { + c.Flush(50000); + Assert.IsTrue(Monitor.Wait(msgLock, 10000)); + } + + Assert.IsTrue(c.Stats.Reconnects == 1); + } + } + } + + int received = 0; + + [TestMethod] + public void TestExtendedReconnectFunctionality() + { + Options opts = reconnectOptions; + + Object disconnectedLock = new Object(); + Object msgLock = new Object(); + Object reconnectedLock = new Object(); + + opts.DisconnectedEventHandler = (sender, args) => + { + System.Console.WriteLine("Disconnected."); + lock (disconnectedLock) + { + Monitor.Pulse(disconnectedLock); + } + }; + + opts.ReconnectedEventHandler = (sender, args) => + { + System.Console.WriteLine("Reconnected."); + lock (reconnectedLock) + { + Monitor.Pulse(reconnectedLock); + } + }; + + byte[] payload = Encoding.UTF8.GetBytes("bar"); + NATSServer ns = utils.CreateServerOnPort(22222); + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + IAsyncSubscription s1 = c.SubscribeAsync("foo"); + IAsyncSubscription s2 = c.SubscribeAsync("foobar"); + + s1.MessageHandler += incrReceivedMessageHandler; + s2.MessageHandler += incrReceivedMessageHandler; + + s1.Start(); + s2.Start(); + + received = 0; + + c.Publish("foo", payload); + c.Flush(); + + lock(disconnectedLock) + { + ns.Shutdown(); + // server is stopped here. + + Assert.IsTrue(Monitor.Wait(disconnectedLock, 20000)); + } + + // subscribe to bar while connected. + IAsyncSubscription s3 = c.SubscribeAsync("bar"); + s3.MessageHandler += incrReceivedMessageHandler; + s3.Start(); + + // Unsub foobar while disconnected + s2.Unsubscribe(); + + c.Publish("foo", payload); + c.Publish("bar", payload); + + // server is restarted here... + using (NATSServer ts = utils.CreateServerOnPort(22222)) + { + // wait for reconnect + lock (reconnectedLock) + { + Assert.IsTrue(Monitor.Wait(reconnectedLock, 60000)); + } + + c.Publish("foobar", payload); + c.Publish("foo", payload); + + using (IAsyncSubscription s4 = c.SubscribeAsync("done")) + { + Object doneLock = new Object(); + s4.MessageHandler += (sender, args) => + { + System.Console.WriteLine("Recieved done message."); + lock (doneLock) + { + Monitor.Pulse(doneLock); + } + }; + + s4.Start(); + + lock (doneLock) + { + c.Publish("done", payload); + Assert.IsTrue(Monitor.Wait(doneLock, 2000)); + } + } + } // NATSServer + + if (received != 4) + { + Assert.Fail("Expected 4, received {0}.", received); + } + } + } + + private void incrReceivedMessageHandler(object sender, + MsgHandlerEventArgs args) + { + System.Console.WriteLine("Received message on subject {0}.", + args.Message.Subject); + Interlocked.Increment(ref received); + } + + [TestMethod] + public void TestQueueSubsOnReconnect() + { + /// implement me. +#if complete_me + +func TestQueueSubsOnReconnect(t *testing.T) { + ts := startReconnectServer(t) + + opts := reconnectOpts + + // Allow us to block on reconnect complete. + reconnectsDone := make(chan bool) + opts.ReconnectedCB = func(nc *nats.Conn) { + reconnectsDone <- true + } + + // Helper to wait on a reconnect. + waitOnReconnect := func() { + select { + case <-reconnectsDone: + break + case <-time.After(2 * time.Second): + t.Fatalf("Expected a reconnect, timedout!\n") + } + } + + // Create connection + nc, _ := opts.Connect() + ec, err := nats.NewEncodedConn(nc, nats.JSON_ENCODER) + if err != nil { + t.Fatalf("Failed to create an encoded connection: %v\n", err) + } + + // To hold results. + results := make(map[int]int) + var mu sync.Mutex + + // Make sure we got what we needed, 1 msg only and all seqnos accounted for.. + checkResults := func(numSent int) { + mu.Lock() + defer mu.Unlock() + + for i := 0; i < numSent; i++ { + if results[i] != 1 { + t.Fatalf("Received incorrect number of messages, [%d] for seq: %d\n", results[i], i) + } + } + + // Auto reset results map + results = make(map[int]int) + } + + subj := "foo.bar" + qgroup := "workers" + + cb := func(seqno int) { + mu.Lock() + defer mu.Unlock() + results[seqno] = results[seqno] + 1 + } + + // Create Queue Subscribers + ec.QueueSubscribe(subj, qgroup, cb) + ec.QueueSubscribe(subj, qgroup, cb) + + ec.Flush() + + // Helper function to send messages and check results. + sendAndCheckMsgs := func(numToSend int) { + for i := 0; i < numToSend; i++ { + ec.Publish(subj, i) + } + // Wait for processing. + ec.Flush() + time.Sleep(50 * time.Millisecond) + + // Check Results + checkResults(numToSend) + } + + // Base Test + sendAndCheckMsgs(10) + + // Stop and restart server + ts.Shutdown() + ts = startReconnectServer(t) + defer ts.Shutdown() + + waitOnReconnect() + + // Reconnect Base Test + sendAndCheckMsgs(10) +} +#endif + } + + //[TestMethod] + public void TestClose() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = "nats://localhost:22222"; + opts.AllowReconnect = true; + opts.MaxReconnect = 60; + + using (NATSServer s1 = utils.CreateServerOnPort(22222)) + { + IConnection c = new ConnectionFactory().CreateConnection(opts); + Assert.IsFalse(c.IsClosed()); + + s1.Shutdown(); + + // FIXME - .NET says still reconnecting. + Thread.Sleep(100); + if (!c.IsClosed()) + { + Assert.Fail("Invalid state, expecting closed, received: " + + c.State.ToString()); + } + + using (NATSServer s2 = utils.CreateServerOnPort(22222)) + { + Thread.Sleep(10000); + Assert.IsFalse(c.IsClosed()); + + c.Close(); + Thread.Sleep(1000); + Assert.IsTrue(c.IsClosed()); + } + } + } + +#if sdlfkjsdflkj + +func TestIsReconnectingAndStatus(t *testing.T) { + ts := startReconnectServer(t) + // This will kill the last 'ts' server that is created + defer func() { ts.Shutdown() }() + disconnectedch := make(chan bool) + reconnectch := make(chan bool) + opts := nats.DefaultOptions + opts.Url = "nats://localhost:22222" + opts.AllowReconnect = true + opts.MaxReconnect = 10000 + opts.ReconnectWait = 100 * time.Millisecond + + opts.DisconnectedCB = func(_ *nats.Conn) { + disconnectedch <- true + } + opts.ReconnectedCB = func(_ *nats.Conn) { + reconnectch <- true + } + + // Connect, verify initial reconnecting state check, then stop the server + nc, err := opts.Connect() + if err != nil { + t.Fatalf("Should have connected ok: %v", err) + } + if nc.IsReconnecting() == true { + t.Fatalf("IsReconnecting returned true when the connection is still open.") + } + if status := nc.Status(); status != nats.CONNECTED { + t.Fatalf("Status returned %d when connected instead of CONNECTED", status) + } + ts.Shutdown() + + // Wait until we get the disconnected callback + if e := Wait(disconnectedch); e != nil { + t.Fatalf("Disconnect callback wasn't triggered: %v", e) + } + if nc.IsReconnecting() == false { + t.Fatalf("IsReconnecting returned false when the client is reconnecting.") + } + if status := nc.Status(); status != nats.RECONNECTING { + t.Fatalf("Status returned %d when reconnecting instead of CONNECTED", status) + } + + ts = startReconnectServer(t) + + // Wait until we get the reconnect callback + if e := Wait(reconnectch); e != nil { + t.Fatalf("Reconnect callback wasn't triggered: %v", e) + } + if nc.IsReconnecting() == true { + t.Fatalf("IsReconnecting returned true after the connection was reconnected.") + } + if status := nc.Status(); status != nats.CONNECTED { + t.Fatalf("Status returned %d when reconnected instead of CONNECTED", status) + } + + // Close the connection, reconnecting should still be false + nc.Close() + if nc.IsReconnecting() == true { + t.Fatalf("IsReconnecting returned true after Close() was called.") + } + if status := nc.Status(); status != nats.CLOSED { + t.Fatalf("Status returned %d after Close() was called instead of CLOSED", status) + } +} + +#endif + + } // class + +} // namespace diff --git a/NATSUnitTests/UnitTestSub.cs b/NATSUnitTests/UnitTestSub.cs new file mode 100755 index 000000000..846159d8b --- /dev/null +++ b/NATSUnitTests/UnitTestSub.cs @@ -0,0 +1,422 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NATS.Client; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace NATSUnitTests +{ + /// + /// Run these tests with the gnatsd auth.conf configuration file. + /// + [TestClass] + public class TestSubscriptions + { + + UnitTestUtilities utils = new UnitTestUtilities(); + + [TestInitialize()] + public void Initialize() + { + utils.StartDefaultServer(); + } + + [TestCleanup()] + public void Cleanup() + { + utils.StopDefaultServer(); + } + + [TestMethod] + public void TestServerAutoUnsub() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + long received = 0; + int max = 10; + + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + s.MessageHandler += (sender, arg) => + { + System.Console.WriteLine("Received msg."); + received++; + }; + + s.AutoUnsubscribe(max); + s.Start(); + + for (int i = 0; i < (max * 2); i++) + { + c.Publish("foo", Encoding.UTF8.GetBytes("hello")); + } + c.Flush(); + + Thread.Sleep(500); + + if (received != max) + { + Assert.Fail("Recieved ({0}) != max ({1})", + received, max); + } + Assert.IsFalse(s.IsValid); + } + } + } + + [TestMethod] + public void TestClientAutoUnsub() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + long received = 0; + int max = 10; + + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + s.AutoUnsubscribe(max); + + for (int i = 0; i < max * 2; i++) + { + c.Publish("foo", null); + } + c.Flush(); + + Thread.Sleep(100); + + try + { + while (true) + { + s.NextMessage(0); + received++; + } + } + catch (NATSBadSubscriptionException) { /* ignore */ } + + Assert.IsTrue(received == max); + Assert.IsFalse(s.IsValid); + } + } + } + + [TestMethod] + public void TestCloseSubRelease() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + Stopwatch sw = new Stopwatch(); + sw.Start(); + try + { + new Task(() => { Thread.Sleep(100); c.Close(); }).Start(); + s.NextMessage(10000); + } + catch (Exception) { /* ignore */ } + + sw.Stop(); + + Assert.IsTrue(sw.ElapsedMilliseconds < 10000); + } + } + } + + [TestMethod] + public void TestValidSubscriber() + { + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + Assert.IsTrue(s.IsValid); + + try { s.NextMessage(100); } + catch (NATSTimeoutException) { } + + Assert.IsTrue(s.IsValid); + + s.Unsubscribe(); + + Assert.IsFalse(s.IsValid); + + try { s.NextMessage(100); } + catch (NATSBadSubscriptionException) { } + } + } + } + + // TODO [TestMethod] + public void TestSlowSubscriber() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.SubChannelLength = 10; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + for (int i =0; i < (opts.SubChannelLength+100); i++) + { + c.Publish("foo", null); + } + + try + { + c.Flush(); + } + catch (Exception ex) + { + System.Console.WriteLine(ex); + if (ex.InnerException != null) + System.Console.WriteLine(ex.InnerException); + + throw ex; + } + + try + { + s.NextMessage(); + } + catch (NATSSlowConsumerException) + { + return; + } + Assert.Fail("Did not receive an exception."); + } + } + } + + [TestMethod] + public void TestSlowAsyncSubscriber() + { + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.SubChannelLength = 10; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + Object mu = new Object(); + + s.MessageHandler += (sender, args) => + { + lock (mu) + { + Console.WriteLine("Subscriber Waiting...."); + Assert.IsTrue(Monitor.Wait(mu, 20000)); + Console.WriteLine("Subscriber done."); + } + }; + + s.Start(); + + for (int i = 0; i < (opts.SubChannelLength + 100); i++) + { + c.Publish("foo", null); + } + + int flushTimeout = 1000; + + Stopwatch sw = new Stopwatch(); + sw.Start(); + + bool flushFailed = false; + try + { + c.Flush(flushTimeout); + } + catch (Exception) + { + flushFailed = true; + } + + sw.Stop(); + + lock (mu) + { + Monitor.Pulse(mu); + } + + if (sw.ElapsedMilliseconds < flushTimeout) + { + Assert.Fail("elapsed ({0}) < timeout ({1})", + sw.ElapsedMilliseconds, flushTimeout); + } + + Assert.IsTrue(flushFailed); + } + } + } + + [TestMethod] + public void TestAsyncErrHandler() + { + Object subLock = new Object(); + object testLock = new Object(); + IAsyncSubscription s; + + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.SubChannelLength = 10; + + bool handledError = false; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + using (s = c.SubscribeAsync("foo")) + { + opts.AsyncErrorEventHandler = (sender, args) => + { + lock (subLock) + { + if (handledError) + return; + + handledError = true; + + Assert.IsTrue(args.Subscription == s); + + System.Console.WriteLine("Expected Error: " + args.Error); + Assert.IsTrue(args.Error.Contains("Slow")); + + // release the subscriber + Monitor.Pulse(subLock); + } + + // release the test + lock (testLock) { Monitor.Pulse(testLock); } + }; + + bool blockedOnSubscriber = false; + s.MessageHandler += (sender, args) => + { + lock (subLock) + { + if (blockedOnSubscriber) + return; + + Console.WriteLine("Subscriber Waiting...."); + Assert.IsTrue(Monitor.Wait(subLock, 10000)); + Console.WriteLine("Subscriber done."); + blockedOnSubscriber = true; + } + }; + + s.Start(); + + lock(testLock) + { + + for (int i = 0; i < (opts.SubChannelLength + 100); i++) + { + c.Publish("foo", null); + } + c.Flush(1000); + + Assert.IsTrue(Monitor.Wait(testLock, 1000)); + } + } + } + } + + [TestMethod] + public void TestAsyncSubscriberStarvation() + { + Object waitCond = new Object(); + + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription helper = c.SubscribeAsync("helper"), + start = c.SubscribeAsync("start")) + { + helper.MessageHandler += (sender, arg) => + { + System.Console.WriteLine("Helper"); + c.Publish(arg.Message.Reply, + Encoding.UTF8.GetBytes("Hello")); + }; + helper.Start(); + + start.MessageHandler += (sender, arg) => + { + System.Console.WriteLine("Responsder"); + string responseIB = c.NewInbox(); + IAsyncSubscription ia = c.SubscribeAsync(responseIB); + + ia.MessageHandler += (iSender, iArgs) => + { + System.Console.WriteLine("Internal subscriber."); + lock (waitCond) { Monitor.Pulse(waitCond); } + }; + ia.Start(); + + c.Publish("helper", responseIB, + Encoding.UTF8.GetBytes("Help me!")); + }; + + start.Start(); + + c.Publish("start", Encoding.UTF8.GetBytes("Begin")); + c.Flush(); + + lock (waitCond) + { + Assert.IsTrue(Monitor.Wait(waitCond, 2000)); + } + } + } + } + + + [TestMethod] + public void TestAsyncSubscribersOnClose() + { + /// basically tests if the subscriber sub channel gets + /// cleared on a close. + Object waitCond = new Object(); + int callbacks = 0; + + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (IAsyncSubscription s = c.SubscribeAsync("foo")) + { + s.MessageHandler += (sender, args) => + { + callbacks++; + lock (waitCond) + { + Monitor.Wait(waitCond); + } + }; + + s.Start(); + + for (int i = 0; i < 10; i++) + { + c.Publish("foo", null); + } + c.Flush(); + + Thread.Sleep(500); + c.Close(); + + lock (waitCond) + { + Monitor.Pulse(waitCond); + } + + Thread.Sleep(500); + + Assert.IsTrue(callbacks == 1); + } + } + } + } // class + +} // namespace diff --git a/NATSUnitTests/UnitTestUtilities.cs b/NATSUnitTests/UnitTestUtilities.cs new file mode 100755 index 000000000..d335ab886 --- /dev/null +++ b/NATSUnitTests/UnitTestUtilities.cs @@ -0,0 +1,184 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; +using System.IO; + +namespace NATSUnitTests +{ + class NATSServer : IDisposable + { + // Enable this for additional server debugging info. + bool debug = false; + Process p; + + public NATSServer() + { + ProcessStartInfo psInfo = createProcessStartInfo(); + this.p = Process.Start(psInfo); + Thread.Sleep(500); + } + + private void addArgument(ProcessStartInfo psInfo, string arg) + { + if (psInfo.Arguments == null) + { + psInfo.Arguments = arg; + } + else + { + string args = psInfo.Arguments; + args += arg; + psInfo.Arguments = args; + } + } + + public NATSServer(int port) + { + ProcessStartInfo psInfo = createProcessStartInfo(); + + addArgument(psInfo, "-p " + port); + + this.p = Process.Start(psInfo); + } + + private TestContext testContextInstance; + /// + ///Gets or sets the test context which provides + ///information about and functionality for the current test run. + /// + public TestContext TestContext + { + get + { + return testContextInstance; + } + set + { + testContextInstance = value; + } + } + + private string buildConfigFileName(string configFile) + { + // TODO: There is a better way with TestContext. + Assembly assembly = Assembly.GetAssembly(this.GetType()); + string codebase = assembly.CodeBase.Replace("file:///", ""); + return Path.GetDirectoryName(codebase) + "\\..\\..\\config\\" + configFile; + } + + public NATSServer(string configFile) + { + ProcessStartInfo psInfo = this.createProcessStartInfo(); + addArgument(psInfo, " -config " + buildConfigFileName(configFile)); + p = Process.Start(psInfo); + } + + private ProcessStartInfo createProcessStartInfo() + { + string gnatsd = Properties.Settings.Default.gnatsd; + ProcessStartInfo psInfo = new ProcessStartInfo(gnatsd); + + if (debug) + { + psInfo.Arguments = " -DV "; + } + else + { + psInfo.WindowStyle = ProcessWindowStyle.Hidden; + } + + return psInfo; + } + + public void Shutdown() + { + if (p == null) + return; + + try + { + p.Kill(); + } + catch (Exception) { } + + p = null; + } + + void IDisposable.Dispose() + { + Shutdown(); + } + } + + class UnitTestUtilities + { + Object mu = new Object(); + static NATSServer defaultServer = null; + Process authServerProcess = null; + + public void StartDefaultServer() + { + lock (mu) + { + if (defaultServer == null) + { + defaultServer = new NATSServer(); + } + } + } + + public void StopDefaultServer() + { + lock (mu) + { + defaultServer.Shutdown(); + defaultServer = null; + } + } + + public void bounceDefaultServer(int delayMillis) + { + StopDefaultServer(); + Thread.Sleep(delayMillis); + StartDefaultServer(); + } + + public void startAuthServer() + { + authServerProcess = Process.Start("gnatsd -config auth.conf"); + } + + internal static void testExpectedException(Action call, Type exType) + { + try { + call.Invoke(); + } + catch (Exception e) + { + System.Console.WriteLine(e); + Assert.IsInstanceOfType(e, exType); + return; + } + + Assert.Fail("No exception thrown!"); + } + + internal NATSServer CreateServerOnPort(int p) + { + return new NATSServer(p); + } + + internal NATSServer CreateServerWithConfig(string configFile) + { + return new NATSServer(configFile); + } + } +} diff --git a/NATSUnitTests/app.config b/NATSUnitTests/app.config new file mode 100755 index 000000000..aca56b49a --- /dev/null +++ b/NATSUnitTests/app.config @@ -0,0 +1,15 @@ + + + + +
+ + + + + + C:\Go\bin\gnatsd.exe + + + + diff --git a/NATSUnitTests/config/auth_1222.conf b/NATSUnitTests/config/auth_1222.conf new file mode 100755 index 000000000..9fb64797d --- /dev/null +++ b/NATSUnitTests/config/auth_1222.conf @@ -0,0 +1,8 @@ +port: 1222 # port to listen for client connections + +# Authorization for client connections +authorization { + user: username + password: password + timeout: 1 +} \ No newline at end of file diff --git a/NATSUnitTests/config/auth_1224.conf b/NATSUnitTests/config/auth_1224.conf new file mode 100755 index 000000000..c729a1337 --- /dev/null +++ b/NATSUnitTests/config/auth_1224.conf @@ -0,0 +1,8 @@ +port: 1224 # port to listen for client connections + +# Authorization for client connections +authorization { + user: username + password: password + timeout: 1 +} \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 840822e7e..8989d46aa --- a/README.md +++ b/README.md @@ -1,2 +1,352 @@ -# csnats -A C# Client for NATS + +# NATS - .NET C# Client +A [C# .NET](https://msdn.microsoft.com/en-us/vstudio/aa496123.aspx) client for the [NATS messaging system](https://nats.io). + +This is an alpha release, based on the [NATS GO Client](https://github.com/nats-io/nats). + +[![License MIT](https://img.shields.io/npm/l/express.svg)](http://opensource.org/licenses/MIT) + +## Installation + +First, download the source code: +``` +git clone git@github.com:nats-io/csnats.git . +``` + +### Quick Start + +Ensure you have installed the .NET Framework 4.0 or greater. Set your path to include csc.exe, e.g. +``` +set PATH=C:\Windows\Microsoft.NET\Framework64\v4.0.30319;%PATH% +``` +Then, build the assembly. There is a simple batch file, build.bat, that will build the assembly (NATS.Client.dll) and the provided examples with only requriing the .NET framework SDK. + +``` +build.bat +``` +The batch file will create a bin directory, and copy all binary files, including samples, into it. + +### Visual Studio + +The recommended alternative is to load NATS.sln into Visual Studio 2013 Express or better. Later versions of Visual Studio should automatically upgrade the solution and project files for you. XML documenation is generated, so code completion, context help, etc, will be available in the editor. + +#### Project files + +The NATS Visual Studio Solution contains several projects, listed below. + +* NATS - The NATS.Client assembly +* NATSUnitTests - Visual Studio Unit Tests (ensure you have gnatds.exe in your path for these). +* Publish Subscribe + * Publish - A sample publisher. + * Subscribe - A sample subscriber. +* QueueGroup - An example queue group subscriber. +* Request Reply + * Requestor - A requestor sample. + * Replier - A sample replier for the Requestor application. + +All examples provide statistics for benchmarking. + +## Basic Usage + +NATS .NET C# Client uses interfaces to reference most NATS client objects, and delegates for all types of events. + +### Creating a NATS .NET Application + +First, reference the NATS.Client assembly so you can use it in your code. Be sure to add a reference in your project or if compiling via command line, compile with the /r:NATS.Client.DLL parameter. While the NATS client is written in C#, any .NET langage can use it. + +Below is some code demonstrating basic API usage. Note that this is example code, not functional as a whole (e.g. requests will fail without a subscriber to reply). + +```C# +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +// Reference the NATS client. +using NATS.Client; +``` + +Here are example snippets of using the API to create a connection, subscribe, publish, and request data. + +```C# + // Create a new connection factory to create + // a connection. + ConnectionFactory cf = new ConnectionFactory(); + + // Creates a live connection to the default + // NATS Server running locally + IConnection c = cf.CreateConnection(); + + // Simple asynchronous subscriber on subject foo + IAsyncSubscription sAsync = c.SubscribeAsync("foo"); + + // Assign a message handler. An anonymous delegate function + // is used for brevity. + sAsync.MessageHandler += (sender, msgArgs) => + { + // print the message + Console.WriteLine(msgArgs.Message); + + // Here are some of the accessible properties from + // the message: + // msgArgs.Message.Data; + // msgArgs.Message.Reply; + // msgArgs.Message.Subject; + // msgArgs.Message.ArrivalSubcription.Subject; + // msgArgs.Message.ArrivalSubcription.QueuedMessageCount; + // msgArgs.Message.ArrivalSubcription.Queue; + + // Unsubscribing from within the delegate function is supported. + msgArgs.Message.ArrivalSubcription.Unsubscribe(); + }; + + // Start the subscriber. Asycnronous subscribers have a + // method to start receiving data. This allows the message handler + // to be setup before data starts arriving; important if multicasting + // delegates. + sAsync.Start(); + + // Simple synchronous subscriber + ISyncSubscription sSync = c.SubscribeSync("foo"); + + // Using a synchronous subscriber, gets the first message available, + // waiting up to 1000 milliseconds (1 second) + Msg m = sSync.NextMessage(1000); + + c.Publish("foo", Encoding.UTF8.GetBytes("hello world")); + + // Unsubscribing + sAsync.Unsubscribe(); + + // Publish requests to the given reply subject: + c.Publish("foo", "bar", Encoding.UTF8.GetBytes("help!")); + + // Sends a request (internally creates an inbox) and Auto-Unsubscribe the + // internal subscriber, which means that the subscriber is unsubscribed + // when receiving the first response from potentially many repliers. + // This call will wait for the reply for up to 1000 milliseconds (1 second). + m = c.Request("foo", Encoding.UTF8.GetBytes("help"), 1000); + + // Closing a connection + c.Close(); +``` + +## Wildcard Subscriptions + +The `*` wildcard matches any token, at any level of the subject: + +```c# +IAsyncSubscription s = c.SubscribeAsync("foo.*.baz"); +``` +This subscriber would receive messages sent to: + +* foo.bar.baz +* foo.a.baz +* etc... + +It would not, however, receive messages on: + +* foo.baz +* foo.baz.bar +* etc... + +The `>` wildcard matches any length of the fail of a subject, and can only be the last token. + +```c# +IAsyncSubscription s = c.SubscribeAsync("foo.>"); +``` +This subscriber would receive any message sent to: + +* foo.bar +* foo.bar.baz +* foo.foo.bar.bax.22 +* etc... + +However, it would not receive messages sent on: + +* foo +* bar.foo.baz +* etc... + +Publishing on this subject would cause the two above subscriber to receive the message: +```c# +c.Publish("foo.bar.baz", null); +``` + +## Queue Groups + +All subscriptions with the same queue name will form a queue group. Each message will be delivered to only one subscriber per queue group, using queue sematics. You can have as many queue groups as you wish. Normal subscribers will continue to work as expected. + +```C# +ISyncSubscription s1 = c.SubscribeSync("foo", "job_workers"); +``` + +or + +```C# +IAsyncSubscription s = c.SubscribeAsync("foo", "job_workers"); +``` + +To unsubscribe, call the ISubscriber Unsubscribe method: +```C# +s.Unsubscribe(); +``` + +When finished with NATS, close the connection. +```C# +c.Close(); +``` + + +## Advanced Usage + +Connection and Subscriber objects implement IDisposable and can be created in a using statement. Here is all the code required to connect to a default server, receive ten messages, and clean up, unsubcribing and closing the connection when finished. + +```C# + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + using (ISyncSubscription s = c.SubscribeSync("foo")) + { + for (int i = 0; i < 10; i++) + { + Msg m = s.NextMessage(); + System.Console.WriteLine("Received: " + m); + } + } + } +``` + +Or to publish ten messages: + +```C# + using (IConnection c = new ConnectionFactory().CreateConnection()) + { + for (int i = 0; i < 10; i++) + { + c.Publish("foo", Encoding.UTF8.GetBytes("hello")); + } + } +``` + +Flush a connection to the server - this call returns when all messages have been processed. Optionally, a timeout in milliseconds can be passed. + +```C# +c.Flush(); + +c.Flush(1000); +``` + +Setup a subscriber to auto-unsubscribe after ten messsages. + +```C# + IAsyncSubscription s = c.SubscribeAsync("foo"); + s.MessageHandler += (sender, args) => + { + Console.WriteLine("Received: " + args.Message); + }; + + s.Start(); + s.AutoUnsubscribe(10); +``` + +Note that an anonymous function was used. This is for brevity here - in practice, delegate functions can be used as well. + +Other events can be assigned delegate methods through the options object. +```C# + Options opts = ConnectionFactory.GetDefaultOptions(); + + opts.AsyncErrorEventHandler += (sender, args) => + { + Console.WriteLine("Error: "); + Console.WriteLine(" Server: " + args.Conn.ConnectedUrl); + Console.WriteLine(" Message: " + args.Error); + Console.WriteLine(" Subject: " + args.Subscription.Subject); + }; + + opts.ClosedEventHandler += (sender, args) => + { + Console.WriteLine("Connection Closed: "); + Console.WriteLine(" Server: " + args.Conn.ConnectedUrl); + }; + + opts.DisconnectedEventHandler += (sender, args) => + { + Console.WriteLine("Connection Disconnected: "); + Console.WriteLine(" Server: " + args.Conn.ConnectedUrl); + }; + + IConnection c = new ConnectionFactory().CreateConnection(opts); +``` + + + +## Clustered Usage + +```C# + string[] servers = new string[] { + "nats://localhost:1222", + "nats://localhost:1224" + }; + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.MaxReconnect = 2; + opts.ReconnectWait = 1000; + opts.NoRandomize = true; + opts.Servers = servers; + + IConnection c = new ConnectionFactory().CreateConnection(opts); +``` + +## Exceptions + +The NATS .NET client can throw the following exceptions: + +* NATSException - The generic NATS exception, and base class for all other NATS exception. +* NATSConnectionException - The exception that is thrown when there is a connection error. +* NATSProtocolException - This exception that is thrown when there is an internal error with the NATS protocol. +* NATSNoServersException - The exception that is thrown when a connection cannot be made to any server. +* NATSSecureConnRequiredException - The exception that is thrown when a secure connection is required. +* NATSConnectionClosedException - The exception that is thrown when a an operation is performed on a connection that is closed. +* NATSSlowConsumerException - The exception that is thrown when a consumer (subscription) is slow. +* NATSStaleConnectionException - The exception that is thrown when an operation occurs on a connection that has been determined to be stale. +* NATSMaxPayloadException - The exception that is thrown when a message payload exceeds what the maximum configured. +* NATSBadSubscriptionException - The exception that is thrown when a subscriber operation is performed on an invalid subscriber. +* NATSTimeoutException - The exception that is thrown when a NATS operation times out. + +## Miscellaneous +Known Issues +* Some unit tests are incomplete or fail. This is due to long connect times with the underlying .NET TCPClient API, issues with the tests themselves, or bugs (This IS an alpha). +* There can be an issue with a flush hanging in some situations. I'm looking into it. + +TODO +* API documentation +* WCF bindings +* Strong name the assembly + + +## License + +(The MIT License) + +Copyright (c) 2012-2015 Apcera Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + diff --git a/build.bat b/build.bat new file mode 100755 index 000000000..4588b03e9 --- /dev/null +++ b/build.bat @@ -0,0 +1,31 @@ + +@setlocal +@echo off + +REM Your version of csc.exe must be in the path. +set PATH=C:\Windows\Microsoft.NET\Framework64\v4.0.30319;%PATH% + +mkdir bin 2>NUL + +cd NATS +csc.exe /nologo /out:..\bin\NATS.Client.DLL /target:library /doc:NATS.XML /optimize+ *.cs Properties\*.cs + +cd ..\examples\publish +csc.exe /nologo /out:..\..\bin\Publish.exe /r:..\..\bin\NATS.Client.dll /target:exe /optimize+ *.cs Properties\*.cs + +cd ..\subscribe +csc.exe /nologo /out:..\..\bin\Subscribe.exe /r:..\..\bin\NATS.Client.dll /target:exe /optimize+ *.cs Properties\*.cs + +cd ..\queuegroup +csc.exe /nologo /out:..\..\bin\Queuegroup.exe /r:..\..\bin\NATS.Client.dll /target:exe /optimize+ *.cs Properties\*.cs + +cd ..\requestor +csc.exe /nologo /out:..\..\bin\Requestor.exe /r:..\..\bin\NATS.Client.dll /target:exe /optimize+ *.cs Properties\*.cs + +cd ..\replier +csc.exe /nologo /out:..\..\bin\replier.exe /r:..\..\bin\NATS.Client.dll /target:exe /optimize+ *.cs Properties\*.cs + +cd ..\..\ + + + diff --git a/examples/Publish/App.config b/examples/Publish/App.config new file mode 100755 index 000000000..8477f5c22 --- /dev/null +++ b/examples/Publish/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/Publish/Properties/AssemblyInfo.cs b/examples/Publish/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..afa5e678a --- /dev/null +++ b/examples/Publish/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Publish")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apcera, Inc.")] +[assembly: AssemblyProduct("Publish")] +[assembly: AssemblyCopyright("Copyright © Apcera 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("38c4999b-852e-4cce-989e-08ea32ae5ca8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/Publish/Publish.csproj b/examples/Publish/Publish.csproj new file mode 100755 index 000000000..c6c0b3841 --- /dev/null +++ b/examples/Publish/Publish.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {A434BE66-EC4D-4FA4-89C7-9097A22319FF} + Exe + Properties + Publish + Publish + v4.5 + 512 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + {68ee71d4-9532-470e-b5ca-ecaa79936b1f} + NATS + + + + + diff --git a/examples/Publish/publish.cs b/examples/Publish/publish.cs new file mode 100755 index 000000000..205937629 --- /dev/null +++ b/examples/Publish/publish.cs @@ -0,0 +1,121 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Collections.Generic; +using NATS.Client; + + +namespace NATSExamples +{ + class Publisher + { + Dictionary parsedArgs = new Dictionary(); + + int count = 2000000; + string url = Defaults.Url; + string subject = "foo"; + byte[] payload = null; + + public void Run(string[] args) + { + Stopwatch sw = null; + + parseArgs(args); + banner(); + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = url; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + sw = Stopwatch.StartNew(); + + for (int i = 0; i < count; i++) + { + c.Publish(subject, payload); + } + c.Flush(); + + sw.Stop(); + + System.Console.Write("Published {0} msgs in {1} seconds ", count, sw.Elapsed.TotalSeconds); + System.Console.WriteLine("({0} msgs/second).", + (int)(count / sw.Elapsed.TotalSeconds)); + printStats(c); + + } + } + + private void printStats(IConnection c) + { + IStatistics s = c.Stats; + System.Console.WriteLine("Statistics: "); + System.Console.WriteLine(" Incoming Payload Bytes: {0}", s.InBytes); + System.Console.WriteLine(" Incoming Messages: {0}", s.InMsgs); + System.Console.WriteLine(" Outgoing Payload Bytes: {0}", s.OutBytes); + System.Console.WriteLine(" Outgoing Messages: {0}", s.OutMsgs); + } + + private void usage() + { + System.Console.Error.WriteLine( + "Usage: Publish [-url url] [-subject subject] " + + "-count [count] [-payload payload]"); + + System.Environment.Exit(-1); + } + + private void parseArgs(string[] args) + { + if (args == null) + return; + + for (int i = 0; i < args.Length; i++) + { + if (i + 1 == args.Length) + usage(); + + parsedArgs.Add(args[i], args[i + 1]); + i++; + } + + if (parsedArgs.ContainsKey("-count")) + count = Convert.ToInt32(parsedArgs["-count"]); + + if (parsedArgs.ContainsKey("-url")) + url = parsedArgs["-url"]; + + if (parsedArgs.ContainsKey("-subject")) + subject = parsedArgs["-subject"]; + + if (parsedArgs.ContainsKey("-payload")) + payload = Encoding.UTF8.GetBytes(parsedArgs["-payload"]); + } + + private void banner() + { + System.Console.WriteLine("Publishing {0} messages on subject {1}", + count, subject); + System.Console.WriteLine(" Url: {0}", url); + System.Console.WriteLine(" Payload is {0} bytes.", + payload != null ? payload.Length : 0); + } + + public static void Main(string[] args) + { + try + { + new Publisher().Run(args); + } + catch (Exception ex) + { + System.Console.Error.WriteLine("Exception: " + ex.Message); + System.Console.Error.WriteLine(ex); + } + + } + } +} diff --git a/examples/QueueGroup/App.config b/examples/QueueGroup/App.config new file mode 100755 index 000000000..8477f5c22 --- /dev/null +++ b/examples/QueueGroup/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/QueueGroup/Properties/AssemblyInfo.cs b/examples/QueueGroup/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..01065c22d --- /dev/null +++ b/examples/QueueGroup/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("QueueGroup")] +[assembly: AssemblyDescription("NATS Subscriber Queue Group Example")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apcera, Inc.")] +[assembly: AssemblyProduct("QueueGroup")] +[assembly: AssemblyCopyright("Copyright © Apcera 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("146547da-36c9-4d1c-9bfe-11a7fa837ab8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/QueueGroup/QueueGroup.cs b/examples/QueueGroup/QueueGroup.cs new file mode 100755 index 000000000..831ff3ddb --- /dev/null +++ b/examples/QueueGroup/QueueGroup.cs @@ -0,0 +1,199 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NATS.Client; + +namespace NATSExamples +{ + class QueueGroup + { + Dictionary parsedArgs = new Dictionary(); + + bool verbose = false; + int count = 1000; + string url = Defaults.Url; + string subject = "foo"; + string qgroup = "worker"; + bool sync = false; + int received = 0; + + public void Run(string[] args) + { + parseArgs(args); + banner(); + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = url; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + TimeSpan elapsed; + + if (sync) + { + elapsed = receiveSyncSubscriber(c); + } + else + { + elapsed = receiveAsyncSubscriber(c); + } + + System.Console.Write("Received {0} msgs in {1} seconds ", count, elapsed.TotalSeconds); + System.Console.WriteLine("({0} msgs/second).", + (int)(count / elapsed.TotalSeconds)); + printStats(c); + + } + } + + private void printStats(IConnection c) + { + IStatistics s = c.Stats; + System.Console.WriteLine("Statistics: "); + System.Console.WriteLine(" Incoming Payload Bytes: {0}", s.InBytes); + System.Console.WriteLine(" Incoming Messages: {0}", s.InMsgs); + System.Console.WriteLine(" Outgoing Payload Bytes: {0}", s.OutBytes); + System.Console.WriteLine(" Outgoing Messages: {0}", s.OutMsgs); + } + + private TimeSpan receiveAsyncSubscriber(IConnection c) + { + Stopwatch sw = new Stopwatch(); + + using (IAsyncSubscription s = c.SubscribeAsync(subject, qgroup)) + { + Object testLock = new Object(); + + s.MessageHandler += (sender, args) => + { + if (received == 0) + sw.Start(); + + received++; + + if (verbose) + Console.WriteLine("Received: " + args.Message); + + if (received >= count) + { + sw.Stop(); + lock (testLock) + { + Monitor.Pulse(testLock); + } + } + }; + + lock (testLock) + { + s.Start(); + Monitor.Wait(testLock); + } + } + + return sw.Elapsed; + } + + private TimeSpan receiveSyncSubscriber(IConnection c) + { + using (ISyncSubscription s = c.SubscribeSync(subject, qgroup)) + { + s.NextMessage(); + received++; + + Stopwatch sw = Stopwatch.StartNew(); + + while (received < count) + { + received++; + Msg m = s.NextMessage(); + if (verbose) + Console.WriteLine("Received Message: " + m); + } + + sw.Stop(); + return sw.Elapsed; + } + } + + private void usage() + { + System.Console.Error.WriteLine( + "Usage: Publish [-url url] [-subject subject] " + + "[-count count] [-queuegroup group] [-sync] [-verbose]"); + + System.Environment.Exit(-1); + } + + private void parseArgs(string[] args) + { + if (args == null) + return; + + for (int i = 0; i < args.Length; i++) + { + if (args[i].Equals("-sync") || + args[i].Equals("-verbose")) + { + parsedArgs.Add(args[i], "true"); + } + else + { + if (i + 1 == args.Length) + usage(); + + parsedArgs.Add(args[i], args[i + 1]); + i++; + } + + } + + if (parsedArgs.ContainsKey("-count")) + count = Convert.ToInt32(parsedArgs["-count"]); + + if (parsedArgs.ContainsKey("-url")) + url = parsedArgs["-url"]; + + if (parsedArgs.ContainsKey("-subject")) + subject = parsedArgs["-subject"]; + + if (parsedArgs.ContainsKey("-queuegroup")) + qgroup = parsedArgs["-queuegroup"]; + + if (parsedArgs.ContainsKey("-verbose")) + verbose = true; + + if (parsedArgs.ContainsKey("-sync")) + sync = true; + } + + private void banner() + { + System.Console.WriteLine("Receiving {0} messages on subject {1}", + count, subject); + System.Console.WriteLine(" Url: {0}", url); + System.Console.WriteLine(" Queue Group: {0}", qgroup); + System.Console.WriteLine(" Receiving: {0}", + sync ? "Synchronously" : "Asynchronously"); + } + + public static void Main(string[] args) + { + try + { + new QueueGroup().Run(args); + } + catch (Exception ex) + { + System.Console.Error.WriteLine("Exception: " + ex.Message); + System.Console.Error.WriteLine(ex); + } + } + } +} diff --git a/examples/QueueGroup/QueueGroup.csproj b/examples/QueueGroup/QueueGroup.csproj new file mode 100755 index 000000000..fe83b784a --- /dev/null +++ b/examples/QueueGroup/QueueGroup.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {E8AECF76-0A83-4128-B2AA-27CBDA9CCA50} + Exe + Properties + QueueGroup + QueueGroup + v4.5 + 512 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + {68ee71d4-9532-470e-b5ca-ecaa79936b1f} + NATS + + + + + diff --git a/examples/Replier/App.config b/examples/Replier/App.config new file mode 100755 index 000000000..8477f5c22 --- /dev/null +++ b/examples/Replier/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/Replier/Properties/AssemblyInfo.cs b/examples/Replier/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..167d58536 --- /dev/null +++ b/examples/Replier/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Replier")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Replier")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4db7dc96-4f6c-4cbe-b5ab-f638994f38c9")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/Replier/Replier.cs b/examples/Replier/Replier.cs new file mode 100755 index 000000000..98908e79b --- /dev/null +++ b/examples/Replier/Replier.cs @@ -0,0 +1,204 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Collections.Generic; +using NATS.Client; + + +namespace NATSExamples +{ + class Replier + { + Dictionary parsedArgs = new Dictionary(); + + int count = 20000; + string url = Defaults.Url; + string subject = "foo"; + bool sync = false; + int received = 0; + bool verbose = false; + Msg replyMsg = new Msg(); + + public void Run(string[] args) + { + parseArgs(args); + banner(); + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = url; + + replyMsg.Data = Encoding.UTF8.GetBytes("reply"); + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + TimeSpan elapsed; + + if (sync) + { + elapsed = receiveSyncSubscriber(c); + } + else + { + elapsed = receiveAsyncSubscriber(c); + } + + System.Console.Write("Replied to {0} msgs in {1} seconds ", count, elapsed.TotalSeconds); + System.Console.WriteLine("({0} replies/second).", + (int)(count / elapsed.TotalSeconds)); + printStats(c); + + } + } + + private void printStats(IConnection c) + { + IStatistics s = c.Stats; + System.Console.WriteLine("Statistics: "); + System.Console.WriteLine(" Incoming Payload Bytes: {0}", s.InBytes); + System.Console.WriteLine(" Incoming Messages: {0}", s.InMsgs); + System.Console.WriteLine(" Outgoing Payload Bytes: {0}", s.OutBytes); + System.Console.WriteLine(" Outgoing Messages: {0}", s.OutMsgs); + } + + + private TimeSpan receiveAsyncSubscriber(IConnection c) + { + Stopwatch sw = new Stopwatch(); + + using (IAsyncSubscription s = c.SubscribeAsync(subject)) + { + Object testLock = new Object(); + + s.MessageHandler += (sender, args) => + { + if (received == 0) + sw.Start(); + + received++; + + if (verbose) + Console.WriteLine("Received: " + args.Message); + + replyMsg.Subject = args.Message.Reply; + c.Publish(replyMsg); + + if (received >= count) + { + sw.Stop(); + lock (testLock) + { + Monitor.Pulse(testLock); + } + } + }; + + lock (testLock) + { + s.Start(); + Monitor.Wait(testLock); + } + } + + return sw.Elapsed; + } + + + private TimeSpan receiveSyncSubscriber(IConnection c) + { + using (ISyncSubscription s = c.SubscribeSync(subject)) + { + s.NextMessage(); + received++; + + Stopwatch sw = Stopwatch.StartNew(); + + while (received < count) + { + received++; + Msg m = s.NextMessage(); + if (verbose) + Console.WriteLine("Received: " + m); + + replyMsg.Subject = m.Reply; + c.Publish(replyMsg); + } + + sw.Stop(); + return sw.Elapsed; + } + } + + private void usage() + { + System.Console.Error.WriteLine( + "Usage: Publish [-url url] [-subject subject] " + + "-count [count] [-sync] [-verbose]"); + + System.Environment.Exit(-1); + } + + private void parseArgs(string[] args) + { + if (args == null) + return; + + for (int i = 0; i < args.Length; i++) + { + if (args[i].Equals("-sync") || + args[i].Equals("-verbose")) + { + parsedArgs.Add(args[i], "true"); + } + else + { + if (i + 1 == args.Length) + usage(); + + parsedArgs.Add(args[i], args[i + 1]); + i++; + } + + } + + if (parsedArgs.ContainsKey("-count")) + count = Convert.ToInt32(parsedArgs["-count"]); + + if (parsedArgs.ContainsKey("-url")) + url = parsedArgs["-url"]; + + if (parsedArgs.ContainsKey("-subject")) + subject = parsedArgs["-subject"]; + + if (parsedArgs.ContainsKey("-sync")) + sync = true; + + if (parsedArgs.ContainsKey("-verbose")) + verbose = true; + } + + private void banner() + { + System.Console.WriteLine("Receiving {0} messages on subject {1}", + count, subject); + System.Console.WriteLine(" Url: {0}", url); + System.Console.WriteLine(" Receiving: {0}", + sync ? "Synchronously" : "Asynchronously"); + } + + public static void Main(string[] args) + { + try + { + new Replier().Run(args); + } + catch (Exception ex) + { + System.Console.Error.WriteLine("Exception: " + ex.Message); + System.Console.Error.WriteLine(ex); + } + } + } +} diff --git a/examples/Replier/Replier.csproj b/examples/Replier/Replier.csproj new file mode 100755 index 000000000..bde4c6795 --- /dev/null +++ b/examples/Replier/Replier.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {BEC6E07B-3DFC-493F-82E6-35A32D52ED2D} + Exe + Properties + Replier + Replier + v4.5 + 512 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + {68ee71d4-9532-470e-b5ca-ecaa79936b1f} + NATS + + + + + diff --git a/examples/Requestor/App.config b/examples/Requestor/App.config new file mode 100755 index 000000000..8477f5c22 --- /dev/null +++ b/examples/Requestor/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/Requestor/Properties/AssemblyInfo.cs b/examples/Requestor/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..fd5601c06 --- /dev/null +++ b/examples/Requestor/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Requestor")] +[assembly: AssemblyDescription("NATS Request Example")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apcera, Inc.")] +[assembly: AssemblyProduct("Requestor")] +[assembly: AssemblyCopyright("Copyright © Apcera 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("83e345f4-308d-473a-8cdf-42d21bd2a5ef")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/Requestor/Requestor.cs b/examples/Requestor/Requestor.cs new file mode 100755 index 000000000..35c38fcb7 --- /dev/null +++ b/examples/Requestor/Requestor.cs @@ -0,0 +1,121 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NATS.Client; + +namespace NATSExamples +{ + class Requestor + { + Dictionary parsedArgs = new Dictionary(); + + int count = 20000; + string url = Defaults.Url; + string subject = "foo"; + byte[] payload = null; + + public void Run(string[] args) + { + Stopwatch sw = null; + + parseArgs(args); + banner(); + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = url; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + sw = Stopwatch.StartNew(); + + for (int i = 0; i < count; i++) + { + c.Request(subject, payload); + } + c.Flush(); + + sw.Stop(); + + System.Console.Write("Completed {0} requests in {1} seconds ", count, sw.Elapsed.TotalSeconds); + System.Console.WriteLine("({0} requests/second).", + (int)(count / sw.Elapsed.TotalSeconds)); + printStats(c); + + } + } + + private void printStats(IConnection c) + { + IStatistics s = c.Stats; + System.Console.WriteLine("Statistics: "); + System.Console.WriteLine(" Incoming Payload Bytes: {0}", s.InBytes); + System.Console.WriteLine(" Incoming Messages: {0}", s.InMsgs); + System.Console.WriteLine(" Outgoing Payload Bytes: {0}", s.OutBytes); + System.Console.WriteLine(" Outgoing Messages: {0}", s.OutMsgs); + } + + private void usage() + { + System.Console.Error.WriteLine( + "Usage: Requestor [-url url] [-subject subject] " + + "-count [count] [-payload payload]"); + + System.Environment.Exit(-1); + } + + private void parseArgs(string[] args) + { + if (args == null) + return; + + for (int i = 0; i < args.Length; i++) + { + if (i + 1 == args.Length) + usage(); + + parsedArgs.Add(args[i], args[i + 1]); + i++; + } + + if (parsedArgs.ContainsKey("-count")) + count = Convert.ToInt32(parsedArgs["-count"]); + + if (parsedArgs.ContainsKey("-url")) + url = parsedArgs["-url"]; + + if (parsedArgs.ContainsKey("-subject")) + subject = parsedArgs["-subject"]; + + if (parsedArgs.ContainsKey("-payload")) + payload = Encoding.UTF8.GetBytes(parsedArgs["-payload"]); + } + + private void banner() + { + System.Console.WriteLine("Sending {0} requests on subject {1}", + count, subject); + System.Console.WriteLine(" Url: {0}", url); + System.Console.WriteLine(" Payload is {0} bytes.", + payload != null ? payload.Length : 0); + } + + public static void Main(string[] args) + { + try + { + new Requestor().Run(args); + } + catch (Exception ex) + { + System.Console.Error.WriteLine("Exception: " + ex.Message); + System.Console.Error.WriteLine(ex); + } + + } + } +} diff --git a/examples/Requestor/Requestor.csproj b/examples/Requestor/Requestor.csproj new file mode 100755 index 000000000..f9c05761a --- /dev/null +++ b/examples/Requestor/Requestor.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {6C2C965C-D1F9-4E5E-863C-EE5C51937866} + Exe + Properties + Requestor + Requestor + v4.5 + 512 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + {68ee71d4-9532-470e-b5ca-ecaa79936b1f} + NATS + + + + + diff --git a/examples/Subscribe/App.config b/examples/Subscribe/App.config new file mode 100755 index 000000000..8477f5c22 --- /dev/null +++ b/examples/Subscribe/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/Subscribe/Properties/AssemblyInfo.cs b/examples/Subscribe/Properties/AssemblyInfo.cs new file mode 100755 index 000000000..d2666a93f --- /dev/null +++ b/examples/Subscribe/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Subscribe")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Apcera, Inc.")] +[assembly: AssemblyProduct("Subscribe")] +[assembly: AssemblyCopyright("Copyright © Apcera 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("cfae3186-01c9-4f56-b4d0-efbd78f25f3b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/examples/Subscribe/Subscribe.csproj b/examples/Subscribe/Subscribe.csproj new file mode 100755 index 000000000..649aa2f36 --- /dev/null +++ b/examples/Subscribe/Subscribe.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {0D44FEE5-87D7-4DC5-956C-B03C3A5B286F} + Exe + Properties + Subscribe + Subscribe + v4.5 + 512 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + {68ee71d4-9532-470e-b5ca-ecaa79936b1f} + NATS + + + + + diff --git a/examples/Subscribe/subscribe.cs b/examples/Subscribe/subscribe.cs new file mode 100755 index 000000000..0b46b83d3 --- /dev/null +++ b/examples/Subscribe/subscribe.cs @@ -0,0 +1,194 @@ +// Copyright 2015 Apcera Inc. All rights reserved. + +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Collections.Generic; +using NATS.Client; + + +namespace NATSExamples +{ + class Subscriber + { + Dictionary parsedArgs = new Dictionary(); + + int count = 1000000; + string url = Defaults.Url; + string subject = "foo"; + bool sync = false; + int received = 0; + bool verbose = false; + + public void Run(string[] args) + { + parseArgs(args); + banner(); + + Options opts = ConnectionFactory.GetDefaultOptions(); + opts.Url = url; + + using (IConnection c = new ConnectionFactory().CreateConnection(opts)) + { + TimeSpan elapsed; + + if (sync) + { + elapsed = receiveSyncSubscriber(c); + } + else + { + elapsed = receiveAsyncSubscriber(c); + } + + System.Console.Write("Received {0} msgs in {1} seconds ", count, elapsed.TotalSeconds); + System.Console.WriteLine("({0} msgs/second).", + (int)(count / elapsed.TotalSeconds)); + printStats(c); + + } + } + + private void printStats(IConnection c) + { + IStatistics s = c.Stats; + System.Console.WriteLine("Statistics: "); + System.Console.WriteLine(" Incoming Payload Bytes: {0}", s.InBytes); + System.Console.WriteLine(" Incoming Messages: {0}", s.InMsgs); + System.Console.WriteLine(" Outgoing Payload Bytes: {0}", s.OutBytes); + System.Console.WriteLine(" Outgoing Messages: {0}", s.OutMsgs); + } + + private TimeSpan receiveAsyncSubscriber(IConnection c) + { + Stopwatch sw = new Stopwatch(); + + using (IAsyncSubscription s = c.SubscribeAsync(subject)) + { + Object testLock = new Object(); + + s.MessageHandler += (sender, args) => + { + if (received == 0) + sw.Start(); + + received++; + + if (verbose) + Console.WriteLine("Received: " + args.Message); + + if (received >= count) + { + sw.Stop(); + lock (testLock) + { + Monitor.Pulse(testLock); + } + } + }; + + lock (testLock) + { + s.Start(); + Monitor.Wait(testLock); + } + } + + return sw.Elapsed; + } + + + private TimeSpan receiveSyncSubscriber(IConnection c) + { + using (ISyncSubscription s = c.SubscribeSync(subject)) + { + s.NextMessage(); + received++; + + Stopwatch sw = Stopwatch.StartNew(); + + while (received < count) + { + received++; + Msg m = s.NextMessage(); + if (verbose) + Console.WriteLine("Received: " + m); + } + + sw.Stop(); + return sw.Elapsed; + } + } + + private void usage() + { + System.Console.Error.WriteLine( + "Usage: Publish [-url url] [-subject subject] " + + "-count [count] [-sync] [-verbose]"); + + System.Environment.Exit(-1); + } + + private void parseArgs(string[] args) + { + if (args == null) + return; + + for (int i = 0; i < args.Length; i++) + { + if (args[i].Equals("-sync") || + args[i].Equals("-verbose")) + { + parsedArgs.Add(args[i], "true"); + } + else + { + if (i + 1 == args.Length) + usage(); + + parsedArgs.Add(args[i], args[i + 1]); + i++; + } + + } + + if (parsedArgs.ContainsKey("-count")) + count = Convert.ToInt32(parsedArgs["-count"]); + + if (parsedArgs.ContainsKey("-url")) + url = parsedArgs["-url"]; + + if (parsedArgs.ContainsKey("-subject")) + subject = parsedArgs["-subject"]; + + if (parsedArgs.ContainsKey("-sync")) + sync = true; + + if (parsedArgs.ContainsKey("-verbose")) + verbose = true; + } + + private void banner() + { + System.Console.WriteLine("Receving {0} messages on subject {1}", + count, subject); + System.Console.WriteLine(" Url: {0}", url); + System.Console.WriteLine(" Receiving: {0}", + sync ? "Synchronously" : "Asynchronously"); + } + + public static void Main(string[] args) + { + try + { + new Subscriber().Run(args); + } + catch (Exception ex) + { + System.Console.Error.WriteLine("Exception: " + ex.Message); + System.Console.Error.WriteLine(ex); + } + } + } +} \ No newline at end of file