Repository: nats-io/ruby-nats Branch: main Commit: 639d4d0dc154 Files: 132 Total size: 394.2 KB Directory structure: gitextract_06zlpyqi/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── config.yml │ ├── defect.yml │ └── proposal.yml ├── .gitignore ├── .travis.yml ├── CODE-OF-CONDUCT.md ├── GOVERNANCE.md ├── Gemfile ├── HISTORY.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── Rakefile ├── TODO ├── benchmark/ │ ├── latency_perf.rb │ ├── pub_perf.rb │ ├── pub_sub_perf.rb │ ├── queues_perf.rb │ ├── sub_perf.rb │ └── sublist_perf.rb ├── bin/ │ ├── nats-pub │ ├── nats-queue │ ├── nats-request │ ├── nats-server │ ├── nats-sub │ └── nats-top ├── dependencies.md ├── examples/ │ ├── auth_pub.rb │ ├── auth_sub.rb │ ├── auto_unsub.rb │ ├── busy_body.rb │ ├── drain_connection.rb │ ├── expected.rb │ ├── fiber_request.rb │ ├── multi_connection.rb │ ├── pub.rb │ ├── queue_sub.rb │ ├── request.rb │ ├── server_config.yml │ ├── server_config_cluster.yml │ ├── simple.rb │ ├── sub.rb │ ├── sub_timeout.rb │ ├── tls-connect.rb │ └── tls.rb ├── lib/ │ └── nats/ │ ├── client.rb │ ├── ext/ │ │ ├── bytesize.rb │ │ ├── em.rb │ │ └── json.rb │ ├── nuid.rb │ ├── server/ │ │ ├── cluster.rb │ │ ├── connection.rb │ │ ├── connz.rb │ │ ├── const.rb │ │ ├── options.rb │ │ ├── route.rb │ │ ├── server.rb │ │ ├── sublist.rb │ │ ├── util.rb │ │ └── varz.rb │ ├── server.rb │ └── version.rb ├── nats.gemspec ├── scripts/ │ └── install_gnatsd.sh └── spec/ ├── .rspec ├── client/ │ ├── attack_spec.rb │ ├── auth_spec.rb │ ├── autounsub_spec.rb │ ├── binary_msg_spec.rb │ ├── client_cluster_config_spec.rb │ ├── client_cluster_reconnect_spec.rb │ ├── client_config_spec.rb │ ├── client_connect_spec.rb │ ├── client_drain_spec.rb │ ├── client_nkeys_connect_spec.rb │ ├── client_requests_spec.rb │ ├── client_spec.rb │ ├── client_tls_spec.rb │ ├── cluster_auth_token_spec.rb │ ├── cluster_auto_discovery_spec.rb │ ├── cluster_lb_spec.rb │ ├── cluster_multi_route_spec.rb │ ├── cluster_retry_connect_spec.rb │ ├── cluster_spec.rb │ ├── error_on_client_spec.rb │ ├── fast_producer_spec.rb │ ├── nuid_spec.rb │ ├── partial_message_spec.rb │ ├── queues_spec.rb │ ├── reconnect_spec.rb │ ├── server_info_spec.rb │ └── sub_timeouts_spec.rb ├── configs/ │ ├── certs/ │ │ ├── bad-ca.pem │ │ ├── ca.pem │ │ ├── client-cert.pem │ │ ├── client-key.pem │ │ ├── key.pem │ │ ├── multi-ca.pem │ │ └── server.pem │ ├── nkeys/ │ │ ├── foo-user.creds │ │ ├── foo-user.jwt │ │ ├── foo-user.nk │ │ └── op.jwt │ ├── tls-no-auth.conf │ ├── tls.conf │ └── tlsverify.conf ├── server/ │ ├── max_connections_spec.rb │ ├── monitor_spec.rb │ ├── multi_user_auth_spec.rb │ ├── protocol_spec.rb │ ├── resources/ │ │ ├── auth.yml │ │ ├── b1_cluster.yml │ │ ├── b2_cluster.yml │ │ ├── cluster.yml │ │ ├── config.yml │ │ ├── max_connections.yml │ │ ├── mixed_auth.yml │ │ ├── monitor.yml │ │ ├── multi_user_auth.yml │ │ ├── multi_user_auth_long.yml │ │ ├── ping.yml │ │ ├── s1_cluster.yml │ │ ├── s2_cluster.yml │ │ └── s3_cluster.yml │ ├── server_cluster_config_spec.rb │ ├── server_config_spec.rb │ ├── server_exitcode_spec.rb │ ├── server_log_spec.rb │ ├── server_ping_spec.rb │ ├── ssl_spec.rb │ └── sublist_spec.rb └── spec_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Discussion url: https://github.com/nats-io/nats.rb/discussions about: Ideal for ideas, feedback, or longer form questions. - name: Chat url: https://slack.nats.io about: Ideal for short, one-off questions, general conversation, and meeting other NATS users! ================================================ FILE: .github/ISSUE_TEMPLATE/defect.yml ================================================ --- name: Defect description: Report a defect, such as a bug or regression. labels: - defect body: - type: textarea id: versions attributes: label: What version were you using? description: Include the server version (`nats-server --version`) and any client versions when observing the issue. validations: required: true - type: textarea id: environment attributes: label: What environment was the server running in? description: This pertains to the operating system, CPU architecture, and/or Docker image that was used. validations: required: true - type: textarea id: steps attributes: label: Is this defect reproducible? description: Provide best-effort steps to showcase the defect. validations: required: true - type: textarea id: expected attributes: label: Given the capability you are leveraging, describe your expectation? description: This may be the expected behavior or performance characteristics. validations: required: true - type: textarea id: actual attributes: label: Given the expectation, what is the defect you are observing? description: This may be an unexpected behavior or regression in performance. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/proposal.yml ================================================ --- name: Proposal description: Propose an enhancement or new feature. labels: - proposal body: - type: textarea id: usecase attributes: label: What motivated this proposal? description: Describe the use case justifying this request. validations: required: true - type: textarea id: change attributes: label: What is the proposed change? description: This could be a behavior change, enhanced API, or a branch new feature. validations: required: true - type: textarea id: benefits attributes: label: Who benefits from this change? description: Describe how this not only benefits you. validations: required: false - type: textarea id: alternates attributes: label: What alternatives have you evaluated? description: This could be using existing features or relying on an external dependency. validations: required: false ================================================ FILE: .gitignore ================================================ *~ \#*\# .\#* *.rbc .rbx .bundle *.gem .DS_Store .idea ================================================ FILE: .travis.yml ================================================ language: ruby rvm: - 2.7 cache: directories: - $HOME/nats-server before_install: - bash ./scripts/install_gnatsd.sh before_script: - export PATH=$HOME/nats-server:$PATH sudo: required dist: xenial bundler_args: --without server ================================================ FILE: CODE-OF-CONDUCT.md ================================================ ## Community Code of Conduct NATS follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). ================================================ FILE: GOVERNANCE.md ================================================ # NATS Ruby Client Governance NATS Ruby Client is part of the NATS project and is subject to the [NATS Governance](https://github.com/nats-io/nats-general/blob/master/GOVERNANCE.md). ================================================ FILE: Gemfile ================================================ source "http://rubygems.org" gemspec group :test do gem 'rake' gem 'rspec' end group :server do gem 'daemons' gem 'json_pure' gem 'thin' gem 'rack', ">= 2.0.6" end group :v2 do gem 'nkeys' end ================================================ FILE: HISTORY.md ================================================ # HISTORY ## v0.11.0 (June 10, 2019) - NATS v2.0 support! (#162) ## v0.8.4 (Feb 23, 2018) - Support to include connection `name` as part of CONNECT options (#145) - Fixed support for Ruby 2.5 due to missing OpenSSL `require` (#144) ## v0.8.2 (March 14, 2017) - Allow setting name from client on connect (#129) - Add discovered servers helper for servers announced via async INFO (#136) - Add time based reconnect backoff (#139) - Modify lang sent on connect when using jruby (#135) - Update eventmachine dependencies (#134) ## v0.8.0 (August 10, 2016) - Added cluster auto discovery handling which is supported on v0.9.2 server release (#125) - Added jruby part of the build (both in openjdk and oraclejdk runtimes) (#122 #123) - Fixed ping interval accounting (#120) ## v0.7.1 (July 8, 2016) - Remove dependencies which are no longer needed for ruby-client - See full list @ https://github.com/nats-io/ruby-nats/compare/v0.7.0...v0.7.1 ## v0.7.0 (July 8, 2016) - Enhanced TLS support: certificates and verify peer functionality added - Bumped version of Eventmachine to 1.2 series - See full list @ https://github.com/nats-io/ruby-nats/compare/v0.6.0...v0.7.0 ## v0.6.0 (March 22, 2016) - Removed distributing `nats-server` along with the gem - Fixed issue with subscriptions not being sent on first reconnect (#94) - Added loading Oj gem for JSON when supported (#91) - Fixed removing warning message introduced by EM 1.0.8 (#90) - Changed to testing spec with `gnatsd` (#95) - See full list @ https://github.com/nats-io/ruby-nats/compare/v0.5.1...v0.6.0 ## v0.5.1 (August 7, 2015) - Changed to never remove servers when configured as such (#88) - See full list @ https://github.com/nats-io/ruby-nats/compare/v0.5.0...v0.5.1 ## v0.5.0 (June 19, 2015) - See full list @ https://github.com/nats-io/ruby-nats/compare/v0.5.0.beta.16...v0.5.0 ## v0.5.0.beta.16 (December 7, 2014) - Resolved major issue on cluster connects to non-first server, issue #78 - Official Support for Ruby 2.1 - See full list @ https://github.com/derekcollison/nats/compare/v0.5.0.beta.12...v0.5.0.beta.16 ## v0.5.0.beta.12 (October 1, 2013) - Fixed issue #58, reconnects not stopped on auth failures - Fixed leaking ping timers on auth failures - Created AuthError - See full list @ https://github.com/derekcollison/nats/compare/v0.5.0.beta.11...v0.5.0.beta.12 ## v0.5.0.beta.11 (July 26, 2013) - Bi-directional Route designation - Upgrade to EM 1.x - See full list @ https://github.com/derekcollison/nats/compare/v0.5.0.beta.1...v0.5.0.beta.11 ## v0.5.0.beta.1 (Sept 10, 2012) - Clustering support for nats-servers - Reconnect client logic cluster aware (explicit servers only for now) - See full list @ https://github.com/derekcollison/nats/compare/v0.4.26...v0.5.0.beta.1 ## v0.4.28 (September 22, 2012) - Binary payload bug fix - Lock EM to version 0.12.10, 1.0 does not pass tests currently. - See full list @ https://github.com/derekcollison/nats/compare/v0.4.26...v0.4.28 ## v0.4.26 (July 30, 2012) - Syslog support - Fixed reconnect bug to authorized servers - See full list @ https://github.com/derekcollison/nats/compare/v0.4.24...v0.4.26 ## v0.4.24 (May 24, 2012) - Persist queue groups across reconnects - Proper exit codes for nats-server - See full list @ https://github.com/derekcollison/nats/compare/v0.4.22...v0.4.24 ## v0.4.22 (Mar 5, 2012) - HTTP based server monitoring (/varz, /connz, /healthz) - Perfomance and Stability improvements - Client monitoring - Server to Client pings - Multiple Auth users - SSL/TSL support - nats-top utility - Connection state dump on SIGUSR2 - Client Server information support - Client Fast Producer support - Client reconenct callbacks - Server Max Connections support - See full list @ https://github.com/derekcollison/nats/compare/v0.4.10...v0.4.22 ## v0.4.10 (Apr 21, 2011) - Minor bug fixes - See full list @ https://github.com/derekcollison/nats/compare/v0.4.8...v0.4.10 ## v0.4.8 (Apr 2, 2011) - Minor bug fixes - See full list @ https://github.com/derekcollison/nats/compare/v0.4.2...v0.4.8 ## v0.4.2 (Feb 21, 2011) - Queue group support - Auto-unsubscribe support - Time expiration on subscriptions - Jruby initial support - Performance enhancements - Complete config file support - See full list @ https://github.com/derekcollison/nats/compare/v0.3.12...v0.4.2 ## v0.3.12 (Nov 21, 2010) - Initial Release ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ # Maintainers Maintainership is on a per project basis. ### Maintainers - Derek Collison [@derekcollison](https://github.com/derekcollison) - Waldemar Quevedo [@wallyqs](https://github.com/wallyqs) ================================================ FILE: README.md ================================================ # NATS - Ruby Client A [Ruby](http://ruby-lang.org) client for the [NATS messaging system](https://nats.io). [![License Apache 2.0](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![Build Status](https://app.travis-ci.com/nats-io/nats.rb.svg?branch=master)](https://app.travis-ci.com/nats-io/nats.rb) [![Gem Version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=rb&type=5&v=0.11.0)](https://rubygems.org/gems/nats/versions/0.11.0) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/nats) ## Getting Started ```bash gem install nats nats-sub foo & nats-pub foo 'Hello World!' ``` Starting from [v0.11.0](https://github.com/nats-io/nats.rb/releases/tag/v0.11.0) release, you can also optionally install [NKEYS](https://github.com/nats-io/nkeys.rb) in order to use the new NATS v2.0 auth features: ```bash gem install nkeys ``` If you're looking for a non-EventMachine alternative, check out the [nats-pure](https://github.com/nats-io/nats-pure.rb) gem. ## Basic Usage ```ruby require "nats/client" NATS.start do # Simple Subscriber NATS.subscribe('foo') { |msg| puts "Msg received : '#{msg}'" } # Simple Publisher NATS.publish('foo.bar.baz', 'Hello World!') # Unsubscribing sid = NATS.subscribe('bar') { |msg| puts "Msg received : '#{msg}'" } NATS.unsubscribe(sid) # Requests NATS.request('help') { |response| puts "Got a response: '#{response}'" } # Replies NATS.subscribe('help') { |msg, reply| NATS.publish(reply, "I'll help!") } # Stop using NATS.stop, exits EM loop if NATS.start started the loop NATS.stop end ``` ## Wildcard Subscriptions ```ruby # "*" matches any token, at any level of the subject. NATS.subscribe('foo.*.baz') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" } NATS.subscribe('foo.bar.*') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" } NATS.subscribe('*.bar.*') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" } # ">" matches any length of the tail of a subject and can only be the last token # E.g. 'foo.>' will match 'foo.bar', 'foo.bar.baz', 'foo.foo.bar.bax.22' NATS.subscribe('foo.>') { |msg, reply, sub| puts "Msg received on [#{sub}] : '#{msg}'" } ``` ## Queues Groups ```ruby # All subscriptions with the same queue name will form a queue group # Each message will be delivered to only one subscriber per queue group, queuing semantics # You can have as many queue groups as you wish # Normal subscribers will continue to work as expected. NATS.subscribe(subject, :queue => 'job.workers') { |msg| puts "Received '#{msg}'" } ``` ## Clustered Usage ```ruby NATS.start(:servers => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223']) do |nc| puts "NATS is connected to #{nc.connected_server}" nc.on_reconnect do puts "Reconnected to server at #{nc.connected_server}" end nc.on_disconnect do |reason| puts "Disconnected: #{reason}" end nc.on_close do puts "Connection to NATS closed" end end opts = { :dont_randomize_servers => true, :reconnect_time_wait => 0.5, :max_reconnect_attempts => 10, :servers => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223', 'nats://127.0.0.1:4224'] } NATS.connect(opts) do |c| puts "NATS is connected!" end ``` ### Auto discovery The client also auto discovers new nodes announced by the server as they attach to the cluster. Reconnection logic parameters such as time to back-off on failure and max attempts apply the same to both discovered nodes and those defined explicitly on connect: ```ruby opts = { :dont_randomize_servers => true, :reconnect_time_wait => 0.5, :max_reconnect_attempts => 10, :servers => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223'], :user => 'secret', :pass => 'deadbeef' } NATS.connect(opts) do |c| # Confirm number of available servers in cluster. puts "Connected to NATS! Servers in pool: #{c.server_pool.count}" end ``` ## Advanced Usage ```ruby # Publish with closure, callback fires when server has processed the message NATS.publish('foo', 'You done?') { puts 'msg processed!' } # Timeouts for subscriptions sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT_IN_SECS) { timeout_recvd = true } # Timeout unless a certain number of messages have been received NATS.timeout(sid, TIMEOUT_IN_SECS, :expected => 2) { timeout_recvd = true } # Auto-unsubscribe after MAX_WANTED messages received NATS.unsubscribe(sid, MAX_WANTED) # Multiple connections NATS.subscribe('test') do |msg| puts "received msg" # Gracefully disconnect from NATS after handling # messages that have already been delivered by server. NATS.drain end # Form second connection to send message on NATS.connect { NATS.publish('test', 'Hello World!') } ``` See examples and benchmarks for more information.. ### TLS Advanced customizations options for setting up a secure connection can be done by including them on connect: ```ruby options = { :servers => [ 'nats://secret:deadbeef@127.0.0.1:4443', 'nats://secret:deadbeef@127.0.0.1:4444' ], :max_reconnect_attempts => 10, :reconnect_time_wait => 2, :tls => { :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem' # Can enable verify_peer functionality optionally by passing # the location of a ca_file. # :verify_peer => true, # :ca_file => './spec/configs/certs/ca.pem' } } # Set default callbacks NATS.on_error do |e| puts "Error: #{e}" end NATS.on_disconnect do |reason| puts "Disconnected: #{reason}" end NATS.on_reconnect do |nats| puts "Reconnected to NATS server at #{nats.connected_server}" end NATS.on_close do puts "Connection to NATS closed" EM.stop end NATS.start(options) do |nats| puts "Connected to NATS at #{nats.connected_server}" nats.subscribe("hello") do |msg| puts "Received: #{msg}" end nats.flush do nats.publish("hello", "world") end end ``` ### Fibers Requests without a callback can be made to work synchronously and return the result when running in a Fiber. For these type of requests, it is possible to set a timeout of how long to wait for a single or multiple responses. ```ruby NATS.start { NATS.subscribe('help') do |msg, reply| puts "[Received]: <<- #{msg}" NATS.publish(reply, "I'll help! - #{msg}") end NATS.subscribe('slow') do |msg, reply| puts "[Received]: <<- #{msg}" EM.add_timer(1) { NATS.publish(reply, "I'll help! - #{msg}") } end 10.times do |n| NATS.subscribe('hi') do |msg, reply| NATS.publish(reply, "Hello World! - id:#{n}") end end Fiber.new do # Requests work synchronously within the same Fiber # returning the message when done. response = NATS.request('help', 'foo') puts "[Response]: ->> '#{response}'" # Specifying a custom timeout to give up waiting for # a response. response = NATS.request('slow', 'bar', timeout: 2) if response.nil? puts "No response after 2 seconds..." else puts "[Response]: ->> '#{response}'" end # Can gather multiple responses with the same request # which will then return a collection with the responses # that were received before the timeout. responses = NATS.request('hi', 'quux', max: 10, timeout: 1) responses.each_with_index do |response, i| puts "[Response# #{i}]: ->> '#{response}'" end # If no replies then an empty collection is returned. responses = NATS.request('nowhere', '', max: 10, timeout: 2) if responses.any? puts "Got #{responses.count} responses" else puts "No response after 2 seconds..." end NATS.stop end.resume # Multiple fibers can make requests concurrently # under the same Eventmachine loop. Fiber.new do 10.times do |n| response = NATS.request('help', "help.#{n}") puts "[Response]: ->> '#{response}'" end end.resume } ``` ### New Authentication (Nkeys and User Credentials) This requires server with version >= 2.0.0 NATS servers have a new security and authentication mechanism to authenticate with user credentials and NKEYS. A single file containing the JWT and NKEYS to authenticate against a NATS v2 server can be set with the `user_credentials` option: ```ruby require 'nats/client' NATS.start("tls://connect.ngs.global", user_credentials: "/path/to/creds") do |nc| nc.subscribe("hello") do |msg| puts "[Received] #{msg}" end nc.publish('hello', 'world') end ``` This will create two callback handlers to present the user JWT and sign the nonce challenge from the server. The core client library never has direct access to your private key and simply performs the callback for signing the server challenge. The library will load and wipe and clear the objects it uses for each connect or reconnect. Bare NKEYS are also supported. The nkey seed should be in a read only file, e.g. `seed.txt`. ```bash > cat seed.txt # This is my seed nkey! SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM ``` Then in the client specify the path to the seed using the `nkeys_seed` option: ```ruby require 'nats/client' NATS.start("tls://connect.ngs.global", nkeys_seed: "path/to/seed.txt") do |nc| nc.subscribe("hello") do |msg| puts "[Received] #{msg}" end nc.publish('hello', 'world') end ``` ## License Unless otherwise noted, the NATS source files are distributed under the Apache Version 2.0 license found in the LICENSE file. ================================================ FILE: Rakefile ================================================ #!/usr/bin/env rake # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'rspec/core' require 'rspec/core/rake_task' task :default => 'spec:client' desc 'Run specs from client and server' RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = FileList['spec/**/*_spec.rb'] spec.rspec_opts = ["--format", "documentation", "--colour", "--profile"] end desc 'Run spec from client using gnatsd as the server' RSpec::Core::RakeTask.new('spec:client') do |spec| spec.pattern = FileList['spec/client/*_spec.rb'] spec.rspec_opts = ["--format", "documentation", "--colour", "--profile"] end desc 'Run spec from client on jruby using gnatsd as the server' RSpec::Core::RakeTask.new('spec:client:jruby') do |spec| spec.pattern = FileList['spec/client/*_spec.rb'] spec.rspec_opts = ["--format", "documentation", "--colour", "--tag", "~jruby_excluded", "--profile"] end desc 'Run spec from server' RSpec::Core::RakeTask.new('spec:server') do |spec| spec.pattern = FileList['spec/server/*_spec.rb'] spec.rspec_opts = ["--format", "documentation", "--colour"] end desc "Build the gem" task :gem do sh 'gem build *.gemspec' end desc "Install the gem" task :geminstall do sh 'gem build *.gemspec' sh 'gem install *.gem' sh 'rm *.gem' end desc "Synonym for spec" task :test => :spec desc "Synonym for spec" task :tests => :spec desc "Synonym for gem" task :pkg => :gem desc "Synonym for gem" task :package => :gem ================================================ FILE: TODO ================================================ - [DONE] Contributing guidelines - [DONE] cluster tests for travis-ci - [DONE] clustering/routing - [DONE] Allow implicit route information to propagate to clients? - [DONE] Check with EM 1.0 not working - [DONE] Client ping timers - [DONE] Retry timers on routes - [DONE] Random on cluster client server pool - do client blowup detection from sending in tight loop - allow port to also be listen_port in config file - queue groups allow selection, e.g. round robin vs random? - Logger rotation should work - [DONE] Allow clients to specify app/client name - [DONE] tests! - [DONE] JRuby support - works if you hand run servers for tests - [DONE] Add a disconnect cb - [DONE] balance receive perf with send perf (EM issue, iovec) - [DONE] Time interval Pings from server.. - [DONE] Ping/Pong time window - [DONE] monitoring - [DONE] Allow monitoring to specify on net param - [DONE] /connz info - [DONE] Don't truncate log on startup - [DONE] Connection queue size dump to log for SIGURS2 - [DONE] Fixed # of replies? sub foo [2], server closes subscription automatically, avoid overwhelming client connection? - [DONE] Request timeout option. - [DONE] proper flow control, EM and PING/PONG - [DONE] request/response automated, just use UUID now. - [DONE] better reply semantics in protocol. - [DONE] daemon mode - [DONE] autolaunch - [DONE] Queue groups ================================================ FILE: benchmark/latency_perf.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' $LOAD_PATH << File.expand_path('../../lib', __FILE__) require 'nats/client' $loop = 10000 $hash = 1000 $sub = 'test' STDOUT.sync = true parser = OptionParser.new do |opts| opts.banner = "Usage: latency_perf [options]" opts.separator "" opts.separator "options:" opts.on("-n ITERATIONS", "iterations to expect (default: #{$loop})") { |iter| $loop = iter.to_i } end parser.parse(ARGV) $drain = $loop trap("TERM") { exit! } trap("INT") { exit! } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do def done ms = "%.2f" % (((Time.now-$start)/$loop)*1000.0) puts "\nTest completed : #{ms} ms avg request/response latency\n" NATS.stop end def send_request s = NATS.request('test') { $drain-=1 if $drain == 0 done else send_request printf('+') if $drain.modulo($hash) == 0 end NATS.unsubscribe(s) } end s_conn = NATS.connect s_conn.subscribe('test') do |msg, reply| s_conn.publish(reply) end # Send first request when we are connected with subscriber s_conn.on_connect { puts "Sending #{$loop} request/responses" $start = Time.now send_request } end ================================================ FILE: benchmark/pub_perf.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' $:.unshift File.expand_path('../../lib', __FILE__) require 'nats/client' $count = 100000 $batch = 100 $delay = 0.00001 $dmin = 0.00001 TRIP = (2*1024*1024) TSIZE = 4*1024 $sub = 'test' $data_size = 16 $hash = 2500 STDOUT.sync = true parser = OptionParser.new do |opts| opts.banner = "Usage: pub_perf [options]" opts.separator "" opts.separator "options:" opts.on("-n COUNT", "Messages to send (default: #{$count}}") { |count| $count = count.to_i } opts.on("-s SIZE", "Message size (default: #{$data_size})") { |size| $data_size = size.to_i } opts.on("-S SUBJECT", "Send subject (default: (#{$sub})") { |sub| $sub = sub } opts.on("-b BATCH", "Batch size (default: (#{$batch})") { |batch| $batch = batch.to_i } end parser.parse(ARGV) trap("TERM") { exit! } trap("INT") { exit! } NATS.on_error { |err| puts "Error: #{err}"; exit! } $data = Array.new($data_size) { "%01x" % rand(16) }.join('').freeze NATS.start(:fast_producer_error => true) do $start = Time.now $to_send = $count $batch = 10 if $data_size >= TSIZE def send_batch (0..$batch).each do $to_send -= 1 if $to_send == 0 NATS.publish($sub, $data) { display_final_results } return else NATS.publish($sub, $data) end printf('+') if $to_send.modulo($hash) == 0 end if (NATS.pending_data_size > TRIP) $delay *= 2 elsif $delay > $dmin $delay /= 2 end EM.add_timer($delay) { send_batch } end def display_final_results elapsed = Time.now - $start mbytes = sprintf("%.1f", (($data_size*$count)/elapsed)/(1024*1024)) puts "\nTest completed : #{($count/elapsed).ceil} msgs/sec (#{mbytes} MB/sec)\n" NATS.stop end if false EM.add_periodic_timer(0.25) do puts "Outstanding data size is #{NATS.client.get_outbound_data_size}" end end puts "Sending #{$count} messages of size #{$data.size} bytes on [#{$sub}]" # kick things off.. send_batch end ================================================ FILE: benchmark/pub_sub_perf.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' $:.unshift File.expand_path('../../lib', __FILE__) require 'nats/client' $count = 100000 $batch = 100 $delay = 0.00001 $dmin = 0.00001 TRIP = (2*1024*1024) TSIZE = 4*1024 $sub = 'test' $data_size = 16 $hash = 2500 STDOUT.sync = true parser = OptionParser.new do |opts| opts.banner = "Usage: pub_perf [options]" opts.separator "" opts.separator "options:" opts.on("-n COUNT", "Messages to send (default: #{$count}}") { |count| $count = count.to_i } opts.on("-s SIZE", "Message size (default: #{$data_size})") { |size| $data_size = size.to_i } opts.on("-S SUBJECT", "Send subject (default: (#{$sub})") { |sub| $sub = sub } opts.on("-b BATCH", "Batch size (default: (#{$batch})") { |batch| $batch = batch.to_i } end parser.parse(ARGV) trap("TERM") { exit! } trap("INT") { exit! } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } $data = Array.new($data_size) { "%01x" % rand(16) }.join('').freeze NATS.start(:fast_producer => true) do $batch = 10 if $data_size >= TSIZE $received = 0 NATS.subscribe($sub) { $received += 1 } $start = Time.now $to_send = $count def send_batch (0..$batch).each do $to_send -= 1 if $to_send == 0 NATS.publish($sub, $data) { display_final_results } return else NATS.publish($sub, $data) end printf('+') if $to_send.modulo($hash) == 0 end if (NATS.pending_data_size > TRIP) $delay *= 2 elsif $delay > $dmin $delay /= 2 end EM.add_timer($delay) { send_batch } end def display_final_results elapsed = Time.now - $start mbytes = sprintf("%.1f", (($data_size*$count)/elapsed)/(1024*1024)) puts "\nTest completed : #{($count/elapsed).ceil} sent/received msgs/sec (#{mbytes} MB/sec)\n" puts "Received #{$received} messages\n" NATS.stop end if false EM.add_periodic_timer(0.25) do puts "Outstanding data size is #{NATS.client.get_outbound_data_size}" end end puts "Sending #{$count} messages of size #{$data.size} bytes on [#{$sub}]" # kick things off.. send_batch end ================================================ FILE: benchmark/queues_perf.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' $:.unshift File.expand_path('../../lib', __FILE__) require 'nats/client' $expected = 100000 $hash = 2500 $sub = 'test' $qs = 5 $qgroup = 'mycoolgroup' STDOUT.sync = true parser = OptionParser.new do |opts| opts.banner = "Usage: queues_perf [options]" opts.separator "" opts.separator "options:" opts.on("-n ITERATIONS", "iterations to expect (default: #{$expected})") { |iter| $expected = iter.to_i } opts.on("-s SUBJECT", "Send subject (default: #{$sub})") { |nsub| $sub = nsub } opts.on("-q QUEUE SUBSCRIBERS", "# subscribers (default: #{$qs})") { |qs| $qs = qs } end parser.parse(ARGV) trap("TERM") { exit! } trap("INT") { exit! } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do received = 1 (0...$qs).each do NATS.subscribe($sub, :queue => $qgroup) do ($start = Time.now and puts "Started Receiving..") if (received == 1) if ((received+=1) == $expected) puts "\nTest completed : #{($expected/(Time.now-$start)).ceil} msgs/sec.\n" NATS.stop end printf('+') if received.modulo($hash) == 0 end end puts "Waiting for #{$expected} messages on [#{$sub}] on #{$qs} queue receivers on group: [#{$qgroup}]" end ================================================ FILE: benchmark/sub_perf.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' $:.unshift File.expand_path('../../lib', __FILE__) require 'nats/client' $expected = 100000 $hash = 2500 $sub = 'test' STDOUT.sync = true parser = OptionParser.new do |opts| opts.banner = "Usage: sub_perf [options]" opts.separator "" opts.separator "options:" opts.on("-n COUNT", "Messages to expect (default: #{$expected})") { |count| $expected = count.to_i } opts.on("-s SUBJECT", "Send subject (default: #{$sub})") { |sub| $sub = sub } end parser.parse(ARGV) trap("TERM") { exit! } trap("INT") { exit! } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do received = 1 NATS.subscribe($sub) { ($start = Time.now and puts "Started Receiving..") if (received == 1) if ((received += 1) == $expected) puts "\nTest completed : #{($expected/(Time.now-$start)).ceil} msgs/sec.\n" NATS.stop end printf('+') if received.modulo($hash) == 0 } puts "Waiting for #{$expected} messages on [#{$sub}]" end ================================================ FILE: benchmark/sublist_perf.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # $:.unshift File.expand_path('../../lib', __FILE__) require 'nats/server/sublist' class PerfSublist @@levels = 5 @@targets = ['derek', 'ruth', 'sam', 'meg', 'brett', 'ben', 'miles', 'bella', 'rex', 'diamond'] @@sublist = Sublist.new() @@subs = [] def PerfSublist.subsInit(pre=nil) @@targets.each {|t| sub = pre ? (pre + "." + t) : t @@sublist.insert(sub, sub) @@subs << sub subsInit(sub) if sub.split(".").size < @@levels } end def PerfSublist.disableSublistCache @@sublist.disable_cache end def PerfSublist.addWildcards @@sublist.insert("ruth.>", "honey") @@sublist.insert("ruth.sam.meg.>", "honey") @@sublist.insert("ruth.*.meg.*", "honey") end def PerfSublist.matchTest(subject, loop) start = Time.now loop.times {@@sublist.match(subject)} stop = Time.now puts "Matched #{subject} #{loop} times in #{ms_time(stop-start)} ms @ #{(loop/(stop-start)).to_i}/sec" end def PerfSublist.reset @@sublist = Sublist.new() end def PerfSublist.removeAll @@subs.each do |sub| @@sublist.remove(sub, sub) end end def PerfSublist.subscriptionCount @@sublist.count end def PerfSublist.totalCount @@subs.count end end def ms_time(t) "%0.2f" % (t*1000) end # setup the subscription list. start = Time.now PerfSublist.subsInit PerfSublist.addWildcards stop = Time.now puts puts "Sublist holding #{PerfSublist.subscriptionCount} subscriptions" puts "Insert rate of #{(PerfSublist.subscriptionCount/(stop-start)).to_i}/sec" #require 'profiler' puts puts "cache test" #Profiler__::start_profile PerfSublist.matchTest("derek.sam.meg.ruth", 100000) PerfSublist.matchTest("ruth.sam.meg.derek", 100000) # multiple returns w/ wc PerfSublist.matchTest("derek.sam.meg.billybob", 100000) # worst case miss #Profiler__::stop_profile #Profiler__::print_profile($stdout) puts puts "Hit any key to continue w/ cache disabled" STDIN.getc #Profiler__::start_profile PerfSublist.disableSublistCache PerfSublist.matchTest("derek.sam.meg.ruth", 50000) PerfSublist.matchTest("ruth.sam.meg.derek", 50000) # multiple returns w/ wc PerfSublist.matchTest("derek.sam.meg.billybob", 50000) # worst case miss #Profiler__::stop_profile #Profiler__::print_profile($stdout) #Run multiple times to see Jruby speedup 0.times do puts "\n\n" #PerfSublist.reset #PerfSublist.subsInit #PerfSublist.disableSublistCache PerfSublist.matchTest("derek.sam.meg.ruth", 50000) PerfSublist.matchTest("ruth.sam.meg.derek", 50000) # multiple returns w/ wc PerfSublist.matchTest("derek.sam.meg.billybob", 50000) # worst case miss end start = Time.now PerfSublist.removeAll stop = Time.now puts puts "Sublist now holding #{PerfSublist.subscriptionCount} subscriptions" puts "Removal rate of #{(PerfSublist.totalCount/(stop-start)).to_i}/sec" # Allows you to see memory usage, etc puts puts "Hit any key to quit" STDIN.getc ================================================ FILE: bin/nats-pub ================================================ #!/usr/bin/env ruby # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' require 'rubygems' require 'nats/client' def usage puts "Usage: nats-pub [-s server] [--creds CREDS]"; exit end args = ARGV.dup opts_parser = OptionParser.new do |opts| opts.on('-s SERVER') { |server| $nats_server = server } opts.on('--creds CREDS') { |creds| $creds = creds } end args = opts_parser.parse!(args) subject, msg = args usage unless subject msg ||= 'Hello World' NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start($nats_server, user_credentials: $creds) do NATS.publish(subject, msg) { NATS.stop } end puts "Published [#{subject}] : '#{msg}'" ================================================ FILE: bin/nats-queue ================================================ #!/usr/bin/env ruby # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' require 'rubygems' require 'nats/client' ['TERM', 'INT'].each { |s| trap(s) { puts; exit! } } def usage puts "Usage: nats-queue [-s server] [-t] [-r]"; exit end args = ARGV.dup opts_parser = OptionParser.new do |opts| opts.on('-s SERVER') { |server| $nats_server = server } opts.on('-t','--time') { $show_time = true } opts.on('-r','--raw') { $show_raw = true } opts.on('--creds CREDS') { |creds| $creds = creds } end args = opts_parser.parse!(args) subject, queue_group = args usage unless subject and queue_group def time_prefix "[#{Time.now}] " if $show_time end def header $i=0 unless $i "#{time_prefix}[\##{$i+=1}]" end def decorate sub, msg if $show_raw msg else "#{header} Received on [#{sub}] : '#{msg}'" end end NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start($nats_server, user_credentials: $creds) do puts "Listening on [#{subject}], queue group [#{queue_group}]" unless $show_raw NATS.subscribe(subject, :queue => queue_group) { |msg, _, sub| puts decorate(sub, msg) } end ================================================ FILE: bin/nats-request ================================================ #!/usr/bin/env ruby # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' require 'rubygems' require 'nats/client' ['TERM', 'INT'].each { |s| trap(s) { puts; exit! } } def usage puts "Usage: nats-request [-s server] [-t] [-r] [-n responses]"; exit end args = ARGV.dup opts_parser = OptionParser.new do |opts| opts.on('-s SERVER') { |server| $nats_server = server } opts.on('-t','--time') { $show_time = true } opts.on('-r','--raw') { $show_raw = true } opts.on('-n RESPONSES') { |responses| $responses = Integer(responses) if Integer(responses) > 0 } opts.on('--creds CREDS') { |creds| $creds = creds } end args = opts_parser.parse!(args) subject, msg = args usage unless subject msg ||= 'Hello World' def time_prefix "[#{Time.now}] " if $show_time end def header $i=0 unless $i "#{time_prefix}[\##{$i+=1}]" end def decorate msg if $show_raw msg else "#{header} Replied with : '#{msg}'" end end NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start($nats_server, user_credentials: $creds) do NATS.request(subject, msg) { |(msg, reply)| puts decorate(msg) exit! if $responses && ($responses-=1) < 1 } end ================================================ FILE: bin/nats-server ================================================ #!/usr/bin/env ruby # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # NATS command line interface script. # Run nats-server -h to get more usage. require 'nats/server' ================================================ FILE: bin/nats-sub ================================================ #!/usr/bin/env ruby # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' require 'rubygems' require 'nats/client' ['TERM', 'INT'].each { |s| trap(s) { puts; exit! } } def usage puts "Usage: nats-sub [-s server] [--creds CREDS] [-t] [-r]"; exit end args = ARGV.dup opts_parser = OptionParser.new do |opts| opts.on('-s SERVER') { |server| $nats_server = server } opts.on('-t','--time') { $show_time = true } opts.on('-r','--raw') { $show_raw = true } opts.on('--creds CREDS') { |creds| $creds = creds } end args = opts_parser.parse!(args) subject = args.shift usage unless subject def time_prefix "[#{Time.now}] " if $show_time end def header $i=0 unless $i "#{time_prefix}[\##{$i+=1}]" end def decorate sub, msg if $show_raw msg else "#{header} Received on [#{sub}] : '#{msg}'" end end NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start($nats_server, user_credentials: $creds) do puts "Listening on [#{subject}]" unless $show_raw NATS.subscribe(subject) { |msg, _, sub| puts decorate(sub, msg) } end ================================================ FILE: bin/nats-top ================================================ #!/usr/bin/env ruby # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'optparse' require 'net/http' require 'uri' require 'io/wait' require 'rubygems' require 'json' def usage puts "Usage: nats-top [-s server_uri] [-m local monitor port] [-n num_connections] [-d delay_secs] [--sort sort_by]" puts "--sort_options for more help" exit end $valid_sort_options = ['pending_size', 'msgs_to', 'msgs_from', 'bytes_to', 'bytes_from', 'subs'] def sort_options_help puts "Available sort_by options: #{$valid_sort_options.join(', ')}." puts "E.g. #{$0} -s bytes_to" exit end args = ARGV.dup opts_parser = OptionParser.new do |opts| opts.on('-s server_uri') { |server| $nats_server = server } opts.on('-m local_port') { |port| $nats_port = port.to_i } opts.on('-n num_connections') { |num| $num_connections = num.to_i } opts.on('-d delay') { |delay| $delay = delay.to_f } opts.on('--sort sort_by') { |sort_key| $sort_key = sort_key } opts.on('--sort_options') { sort_options_help } opts.on('-h') { usage } opts.on('--help') { usage } end args = opts_parser.parse!(args) DEFAULT_MONITOR_PORT = 9222 DEFAULT_NUM_CONNECTIONS = 10 DEFAULT_DELAY = 1 #sec DEFAULT_SORT = 'pending_size' $nats_port = DEFAULT_MONITOR_PORT if $nats_port.nil? $num_connections = DEFAULT_NUM_CONNECTIONS if $num_connections.nil? $nats_server = "http://localhost:#{$nats_port}" if $nats_server.nil? $nats_server = "http://#{$nats_server}" unless $nats_server.start_with?('http') $delay = DEFAULT_DELAY if $delay.nil? $sort_key = DEFAULT_SORT if $sort_key.nil? $sort_key.downcase! unless $valid_sort_options.include?($sort_key) puts "Invalid sort_by argument: #{$sort_key}" sort_options_help end varz_uri = URI.parse("#{$nats_server}/varz") connz_uri = URI.parse("#{$nats_server}/connz?n=#{$num_connections}&s=#{$sort_key}") def psize(size, prec=1) return 'NA' unless size return sprintf("%.#{prec}f", size) if size < 1024 return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024) return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024) return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0)) end def clear_screen print "\e[H\e[2J" end ['TERM', 'INT'].each { |s| trap(s) { clear_screen; exit! } } in_last_msgs = in_last_bytes = 0 out_last_msgs = out_last_bytes = 0 poll = Time.now first = true while true begin varz_response = Net::HTTP::get_response(varz_uri) varz = JSON.parse(varz_response.body, :symbolize_keys => true, :symbolize_names => true) # Simple rates delta_in_msgs, in_last_msgs = varz[:in_msgs] - in_last_msgs, varz[:in_msgs] delta_in_bytes, in_last_bytes = varz[:in_bytes] - in_last_bytes, varz[:in_bytes] delta_out_msgs, out_last_msgs = varz[:out_msgs] - out_last_msgs, varz[:out_msgs] delta_out_bytes, out_last_bytes = varz[:out_bytes] - out_last_bytes, varz[:out_bytes] now = Time.now tdelta, poll = now - poll, now unless first rate_in_msgs = delta_in_msgs / tdelta rate_in_bytes = delta_in_bytes / tdelta rate_out_msgs = delta_out_msgs / tdelta rate_out_bytes = delta_out_bytes / tdelta end connz_response = Net::HTTP::get_response(connz_uri) connz = JSON.parse(connz_response.body, :symbolize_keys => true, :symbolize_names => true) clear_screen puts "\nServer:" puts " Load: CPU: #{varz[:cpu]}% Memory: #{psize(varz[:mem])}" print " In: Msgs: #{psize(varz[:in_msgs])} Bytes: #{psize(varz[:in_bytes])}" puts " Msgs/Sec: #{psize(rate_in_msgs)} Bytes/Sec: #{psize(rate_in_bytes)}" print " Out: Msgs: #{psize(varz[:out_msgs])} Bytes: #{psize(varz[:out_bytes])}" puts " Msgs/Sec: #{psize(rate_out_msgs)} Bytes/Sec: #{psize(rate_out_bytes)}" puts "\nConnections: #{psize(connz[:num_connections], 0)}" conn_t = " %-20s %-8s %-6s %-10s %-10s %-10s %-10s %-10s\n" printf(conn_t, 'HOST', 'CID', 'SUBS', 'PENDING', 'MSGS_TO', 'MSGS_FROM', 'BYTES_TO', 'BYTES_FROM') connz[:connections].each do |conn| printf(conn_t, "#{conn[:ip]}:#{conn[:port]}", conn[:cid], psize(conn[:subscriptions]), psize(conn[:pending_size]), psize(conn[:out_msgs]), psize(conn[:in_msgs]), psize(conn[:out_bytes]), psize(conn[:in_bytes]) ) end puts first = false sleep($delay) rescue => e puts "Error: #{e}" exit(1) end end ================================================ FILE: dependencies.md ================================================ # External Dependencies This file lists the dependencies used in this repository. | Dependency | License | |-|-| | github.com/eventmachine/eventmachine | Ruby License | ================================================ FILE: examples/auth_pub.rb ================================================ require 'rubygems' require 'nats/client' def usage puts "Usage: pub.rb "; exit end user, pass, subject, msg = ARGV usage unless user and pass and subject # Default msg ||= 'Hello World' uri = "nats://#{user}:#{pass}@localhost:#{NATS::DEFAULT_PORT}" NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start(:uri => uri) do NATS.publish(subject, msg) { NATS.stop } end puts "Published on [#{subject}] : '#{msg}'" ================================================ FILE: examples/auth_sub.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } def usage puts "Usage: auth_sub "; exit end user, pass, subject = ARGV usage unless user and pass and subject uri = "nats://#{user}:#{pass}@localhost:#{NATS::DEFAULT_PORT}" NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start(:uri => uri) do puts "Listening on [#{subject}]" NATS.subscribe(subject) { |msg, _, sub| puts "Received on [#{sub}] : '#{msg}'" } end ================================================ FILE: examples/auto_unsub.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } def usage puts "Usage: ruby auto_unsub.rb [wanted=5] [send=10]"; exit end subject = ARGV.shift wanted = ARGV.shift || 5 send = ARGV.shift || 10 usage unless subject NATS.on_error { |err| puts "Server Error: #{err}"; exit! } received = 0 NATS.start do puts "Listening on [#{subject}], auto unsubscribing after #{wanted} messages, but will send #{send}." NATS.subscribe(subject, :max => wanted) { |msg| puts "Received '#{msg}'" received += 1 } (0...send).each { NATS.publish(subject, 'hello') } NATS.publish('done') { NATS.stop } end puts "Received #{received} messages" ================================================ FILE: examples/busy_body.rb ================================================ require 'rubygems' require 'nats/client' # This is an example to show off nats-top. Run busy_body on a monitor enabled # server and exec nats-top. ['TERM', 'INT'].each { |sig| trap(sig) { exit! } } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } def create_subscribers(sub='foo.bar', num_subs=10, num_connections=20) (1..num_connections).each do NATS.connect do |nc| (1..num_subs).each { nc.subscribe(sub) } end end end def create_publishers(sub='foo.bar', body='Hello World!', num_connections=20, num_sends=100) (1..num_connections).each do NATS.connect do |nc| (1..num_sends).each { nc.publish(sub, body) } end end end def timed_publish(sub='foo.bar', body='Hello World!', delay=1, burst=500) EM.add_periodic_timer(1) do b = (burst * rand).to_i (1..b).each { NATS.publish(sub, body) } end end NATS.start { create_subscribers create_publishers timed_publish } ================================================ FILE: examples/drain_connection.rb ================================================ require 'nats/client' nc1 = nil nc2 = nil responses = [] inbox = NATS.create_inbox ["TERM", "INT"].each { |sig| trap(sig) { EM.stop } } subscribers = [] EM.run do 5.times do |n| subscribers << NATS.connect(drain_timeout: 30, name: "client-#{n}") do |nc| nc.on_error { |err| puts "#{Time.now} - Error: #{err}" } nc.on_close { |err| puts "#{Time.now} - Connection drained and closed!" } puts "#{Time.now} - Started Connection #{n}..." nc.flush do nc.subscribe('foo', queue: "workers") do |msg, reply, sub| nc.publish(reply, "ACK1:#{msg}") end nc.subscribe('bar', queue: "workers") do |msg, reply, sub| nc.publish(reply, "ACK1:#{msg}") end nc.subscribe('quux', queue: "workers") do |msg, reply, sub| nc.publish(reply, "ACK1:#{msg}") end end end end pub_client = NATS.connect do |nc| EM.add_periodic_timer(0.001) do Fiber.new do response = nc.request("foo", "A") puts "Dropped request!!!" if response.nil? end.resume end EM.add_periodic_timer(0.001) do Fiber.new do response = nc.request("bar", "B") puts "Dropped request!!!" if response.nil? # puts "Response on 'bar' : #{response}" end.resume end EM.add_periodic_timer(0.001) do Fiber.new do response = nc.request("quux", "C") puts "Dropped request!!!" if response.nil? end.resume end end EM.add_timer(1) do # Drain is like stop but gracefully closes the connection. subs = subscribers[0..3] subs.each_with_index do |nc, i| if nc.draining? puts "Already draining... #{responses.count}" next end # Just using close will cause some requests to fail # nc.close # Drain is more graceful and allow clients to process requests # that have already been delivered by the server to the subscriber. puts "#{Time.now} - Start draining #{nc.options[:name]}... (pending_data: #{nc.pending_data_size})" nc.drain do puts "#{Time.now} - Done draining #{nc.options[:name]}!" end end end end ================================================ FILE: examples/expected.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } def usage puts "Usage: ruby expected.rb [timeout (default 5 secs)] [expected (default 5)]" exit end subject = ARGV.shift timeout = ARGV.shift || 5 expected = ARGV.shift || 5 usage unless subject NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do received = 0 puts "Listening on [#{subject}]" puts "Will timeout in #{timeout} seconds unless #{expected} messages are received." sid = NATS.subscribe(subject) { |msg| puts "Received '#{msg}'" received += 1 if received >= expected puts "All #{expected} messages received, exiting.." NATS.stop end } NATS.timeout(sid, timeout, :expected => expected) { puts "Timedout waiting for a message!" NATS.stop } end ================================================ FILE: examples/fiber_request.rb ================================================ require 'fiber' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { EM.stop } } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start { NATS.subscribe('help') do |msg, reply| puts "[Received]: <<- #{msg}" NATS.publish(reply, "I'll help! - #{msg}") end NATS.subscribe('slow') do |msg, reply| puts "[Received]: <<- #{msg}" EM.add_timer(1) { NATS.publish(reply, "I'll help! - #{msg}") } end 10.times do |n| NATS.subscribe('hi') do |msg, reply| NATS.publish(reply, "Hello World! - id:#{n}") end end Fiber.new do # Requests work synchronously within the same Fiber # returning the message when done. response = NATS.request('help', 'foo') puts "[Response]: ->> '#{response}'" # Specifying a custom timeout to give up waiting for # a response. response = NATS.request('slow', 'bar', timeout: 2) if response.nil? puts "No response after 2 seconds..." else puts "[Response]: ->> '#{response}'" end # Can gather multiple responses with the same request # which will then return a collection with the responses # that were received before the timeout. responses = NATS.request('hi', 'quux', max: 10, timeout: 1) responses.each_with_index do |response, i| puts "[Response# #{i}]: ->> '#{response}'" end # If no replies then an empty collection is returned. responses = NATS.request('nowhere', '', max: 10, timeout: 2) if responses.any? puts "Got #{responses.count} responses" else puts "No response after 2 seconds..." end NATS.stop end.resume # Multiple fibers can make requests concurrently # under the same Eventmachine loop. Fiber.new do 10.times do |n| response = NATS.request('help', "help.#{n}") puts "[Response]: ->> '#{response}'" end end.resume } ================================================ FILE: examples/multi_connection.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start { NATS.subscribe('test') do |msg, reply, sub| puts "received data on sub:#{sub} - #{msg}" NATS.stop end # Form a second connection to send message on NATS.connect { |nc| nc.publish('test', 'Hello World!') } } ================================================ FILE: examples/pub.rb ================================================ require 'rubygems' require 'nats/client' def usage puts "Usage: ruby pub.rb "; exit end subject, msg = ARGV usage unless subject msg ||= 'Hello World' NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start { NATS.publish(subject, msg) { NATS.stop } } puts "Published [#{subject}] : '#{msg}'" ================================================ FILE: examples/queue_sub.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } def usage puts "Usage: ruby queue_sub.rb "; exit end subject, queue_group = ARGV usage unless subject and queue_group NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do puts "Listening on [#{subject}], queue group [#{queue_group}]" NATS.subscribe(subject, :queue => queue_group) { |msg| puts "Received '#{msg}'" } end ================================================ FILE: examples/request.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start { # The helper NATS.subscribe('help') do |msg, reply| NATS.publish(reply, "I'll help!") end # Help request NATS.request('help') { |response| puts "Got a response: '#{response}'" NATS.stop } } ================================================ FILE: examples/server_config.yml ================================================ --- # # Sample Server Sonfiguration # nats-server -c ./server_config.yml # port: 4242 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 ssl: false pid_file: '/tmp/nats_test.pid' # log_file: '/tmp/nats_test.log' # Debug Options logtime: true debug: false trace: false # Protocol/Limits max_control_line: 512 max_payload: 512000 max_pending: 2000000 # EM/IO no_epoll: false no_kqueue: true ================================================ FILE: examples/server_config_cluster.yml ================================================ --- # # Sample Server Sonfiguration # nats-server -c ./server_config.yml # port: 4242 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. # # NATS can support both full mesh and directive # acyclic graphs setups. Its up to the configuration # setup to avoid cycles. # # The port definition allows us to receive incoming connections. # Comment out if you want to suppress incoming connections. # # The server can solicit active connections via the routes definitions below. # # authorization is similar to client connection definitions. cluster: port: 4244 authorization: user: route_user password: cafebabe token: deadbeef timeout: 1 # These are actively connected from this server. Other servers # can connect to us if they supply the correct credentials from # above. routes: nats-route://foo:bar@127.0.0.1:4220 nats-route://foo:bar@127.0.0.1:4221 pid_file: '/tmp/nats_test.pid' # log_file: '/tmp/nats_test.log' # Debug Options logtime: true debug: false trace: false # Protocol/Limits max_control_line: 512 max_payload: 512000 max_pending: 2000000 # EM/IO no_epoll: false no_kqueue: true ================================================ FILE: examples/simple.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start { NATS.subscribe('test') do |msg, reply, sub| puts "received data on sub:#{sub} - #{msg}" NATS.stop end NATS.publish('test', 'Hello World!') } ================================================ FILE: examples/sub.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } def usage puts "Usage: ruby sub.rb "; exit end subject = ARGV.shift usage unless subject NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do puts "Listening on [#{subject}]" NATS.subscribe(subject) { |msg| puts "Received '#{msg}'" } end ================================================ FILE: examples/sub_timeout.rb ================================================ require 'rubygems' require 'nats/client' ["TERM", "INT"].each { |sig| trap(sig) { NATS.stop } } def usage puts "Usage: ruby subtimeout.rb [timeout (default 5 secs)]"; exit end subject = ARGV.shift timeout = ARGV.shift || 5 usage unless subject NATS.on_error { |err| puts "Server Error: #{err}"; exit! } NATS.start do puts "Listening on [#{subject}]" puts "Will timeout in #{timeout} seconds." sid = NATS.subscribe(subject) { |msg| puts "Received '#{msg}'" NATS.stop } NATS.timeout(sid, timeout) { puts "Timedout waiting for a message!" NATS.stop } end ================================================ FILE: examples/tls-connect.rb ================================================ require 'nats/client' EM.run do options = { :servers => [ 'nats://secret:deadbeef@127.0.0.1:4443', 'nats://secret:deadbeef@127.0.0.1:4444' ], :max_reconnect_attempts => 10, :reconnect_time_wait => 2, :tls => { :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem' } } NATS.connect(options) do |nc| puts "#{Time.now.to_f} - Connected to NATS at #{nc.connected_server}" nc.subscribe("hello") do |msg| puts "#{Time.now.to_f} - Received: #{msg}" end nc.flush do nc.publish("hello", "world") end EM.add_periodic_timer(0.1) do next unless nc.connected? nc.publish("hello", "hello") end # Set default callbacks nc.on_error do |e| puts "#{Time.now.to_f } - Error: #{e}" end nc.on_disconnect do |reason| puts "#{Time.now.to_f} - Disconnected: #{reason}" end nc.on_reconnect do |nc| puts "#{Time.now.to_f} - Reconnected to NATS server at #{nc.connected_server}" end nc.on_close do puts "#{Time.now.to_f} - Connection to NATS closed" EM.stop end end end ================================================ FILE: examples/tls.rb ================================================ require 'nats/client' options = { :servers => [ 'nats://secret:deadbeef@127.0.0.1:4443', 'nats://secret:deadbeef@127.0.0.1:4444' ], :max_reconnect_attempts => 10, :reconnect_time_wait => 2, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem' } } # Set default callbacks NATS.on_error do |e| puts "#{Time.now.to_f } - Error: #{e}" end NATS.on_disconnect do |reason| puts "#{Time.now.to_f} - Disconnected: #{reason}" end NATS.on_reconnect do |nats| puts "#{Time.now.to_f} - Reconnected to NATS server at #{nats.connected_server}" end NATS.on_close do puts "#{Time.now.to_f} - Connection to NATS closed" EM.stop end NATS.start(options) do |nats| puts "#{Time.now.to_f} - Connected to NATS at #{nats.connected_server}" nats.subscribe("hello") do |msg| puts "#{Time.now.to_f} - Received: #{msg}" end nats.flush do nats.publish("hello", "world") end EM.add_periodic_timer(0.1) do next unless nats.connected? nats.publish("hello", "hello") end end ================================================ FILE: lib/nats/client.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'uri' require 'securerandom' require 'fiber' require 'openssl' unless defined?(OpenSSL) ep = File.expand_path(File.dirname(__FILE__)) require "#{ep}/ext/em" require "#{ep}/ext/bytesize" require "#{ep}/ext/json" require "#{ep}/version" require "#{ep}/nuid" module NATS DEFAULT_PORT = 4222 DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze MAX_RECONNECT_ATTEMPTS = 10 RECONNECT_TIME_WAIT = 2 MAX_PENDING_SIZE = 32768 # Maximum outbound size per client to trigger FP, 20MB FAST_PRODUCER_THRESHOLD = (10*1024*1024) # Ping intervals DEFAULT_PING_INTERVAL = 120 DEFAULT_PING_MAX = 2 # Drain mode support DEFAULT_DRAIN_TIMEOUT = 30 # Protocol # @private MSG = /\AMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i #:nodoc: OK = /\A\+OK\s*\r\n/i #:nodoc: ERR = /\A-ERR\s+('.+')?\r\n/i #:nodoc: PING = /\APING\s*\r\n/i #:nodoc: PONG = /\APONG\s*\r\n/i #:nodoc: INFO = /\AINFO\s+([^\r\n]+)\r\n/i #:nodoc: UNKNOWN = /\A(.*)\r\n/ #:nodoc: # Responses CR_LF = ("\r\n".freeze) #:nodoc: CR_LF_SIZE = (CR_LF.bytesize) #:nodoc: PING_REQUEST = ("PING#{CR_LF}".freeze) #:nodoc: PONG_RESPONSE = ("PONG#{CR_LF}".freeze) #:nodoc: SUB_OP = ('SUB'.freeze) #:nodoc: EMPTY_MSG = (''.freeze) #:nodoc: # Used for future pedantic Mode SUB = /^([^\.\*>\s]+|>$|\*)(\.([^\.\*>\s]+|>$|\*))*$/ #:nodoc: SUB_NO_WC = /^([^\.\*>\s]+)(\.([^\.\*>\s]+))*$/ #:nodoc: # Parser AWAITING_CONTROL_LINE = 1 #:nodoc: AWAITING_MSG_PAYLOAD = 2 #:nodoc: AWAITING_INFO_LINE = 3 # :nodoc: class Error < StandardError; end #:nodoc: # When the NATS server sends us an ERROR message, this is raised/passed by default class ServerError < Error; end #:nodoc: # When we detect error on the client side (e.g. Fast Producer, TLS required) class ClientError < Error; end #:nodoc: # When we cannot connect to the server (either initially or after a reconnect), this is raised/passed class ConnectError < Error; end #:nodoc: # When we cannot connect to the server because authorization failed. class AuthError < ConnectError; end #:nodoc: class << self attr_reader :client, :reactor_was_running, :err_cb, :err_cb_overridden #:nodoc: attr_reader :reconnect_cb, :close_cb, :disconnect_cb #:nodoc alias :reactor_was_running? :reactor_was_running # Create and return a connection to the server with the given options. # The optional block will be called when the connection has been completed. # # @param [String] uri The URI or comma separated list of URIs of NATS servers to connect to. # @param [Hash] opts # @option opts [String|URI] :uri The URI to connect to, example nats://localhost:4222 # @option opts [Boolean] :reconnect Boolean that can be used to suppress reconnect functionality. # @option opts [Boolean] :debug Boolean that can be used to output additional debug information. # @option opts [Boolean] :verbose Boolean that is sent to server for setting verbose protocol mode. # @option opts [Boolean] :pedantic Boolean that is sent to server for setting pedantic mode. # @option opts [Boolean] :ssl Boolean that is sent to server for setting TLS/SSL mode. # @option opts [Hash] :tls Map of options for configuring secure connection handled to EM#start_tls directly. # @option opts [Integer] :max_reconnect_attempts Integer that can be used to set the max number of reconnect tries # @option opts [Integer] :reconnect_time_wait Integer that can be used to set the number of seconds to wait between reconnect tries # @option opts [Integer] :ping_interval Integer that can be used to set the ping interval in seconds. # @option opts [Integer] :max_outstanding_pings Integer that can be used to set the max number of outstanding pings before declaring a connection closed. # @param [Block] &blk called when the connection is completed. Connection will be passed to the block. # @return [NATS] connection to the server. # # @example Connect to local NATS server. # NATS.connect do |nc| # # ... # end # # @example Setting custom server URI to connect. # NATS.connect("nats://localhost:4222") do |nc| # # ... # end # # @example Setting username and password to authenticate. # NATS.connect("nats://user:password@localhost:4222") do |nc| # # ... # end # # @example Specifying explicit list of servers via options. # NATS.connect(servers: ["nats://127.0.0.1:4222","nats://127.0.0.1:4223","nats://127.0.0.1:4224"]) do |nc| # # ... # end # # @example Using comma separated array to define list of servers. # NATS.connect("nats://localhost:4223,nats://localhost:4224") do |nc| # # ... # end # # @example Only specifying endpoint uses NATS default scheme and port. # NATS.connect("demo.nats.io") do |nc| # # ... # end # # @example Setting infinite reconnect retries with 2 seconds back off against custom URI. # NATS.connect("demo.nats.io:4222", max_reconnect_attempts: -1, reconnect_time_wait: 2) do |nc| # # ... # end # def connect(uri=nil, opts={}, &blk) case uri when String # Initialize TLS defaults in case any url is using it. uris = opts[:uri] = process_uri(uri) opts[:tls] ||= {} if uris.any? {|u| u.scheme == 'tls'} when Hash opts = uri end # Defaults opts[:verbose] = false if opts[:verbose].nil? opts[:pedantic] = false if opts[:pedantic].nil? opts[:reconnect] = true if opts[:reconnect].nil? opts[:ssl] = false if opts[:ssl].nil? opts[:max_reconnect_attempts] = MAX_RECONNECT_ATTEMPTS if opts[:max_reconnect_attempts].nil? opts[:reconnect_time_wait] = RECONNECT_TIME_WAIT if opts[:reconnect_time_wait].nil? opts[:ping_interval] = DEFAULT_PING_INTERVAL if opts[:ping_interval].nil? opts[:max_outstanding_pings] = DEFAULT_PING_MAX if opts[:max_outstanding_pings].nil? opts[:drain_timeout] = DEFAULT_DRAIN_TIMEOUT if opts[:drain_timeout].nil? # Override with ENV opts[:uri] ||= ENV['NATS_URI'] || DEFAULT_URI opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil? opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil? opts[:debug] = ENV['NATS_DEBUG'].downcase == 'true' unless ENV['NATS_DEBUG'].nil? opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil? opts[:fast_producer_error] = ENV['NATS_FAST_PRODUCER'].downcase == 'true' unless ENV['NATS_FAST_PRODUCER'].nil? opts[:ssl] = ENV['NATS_SSL'].downcase == 'true' unless ENV['NATS_SSL'].nil? opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil? opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil? opts[:name] ||= ENV['NATS_CONNECTION_NAME'] opts[:no_echo] ||= ENV['NATS_NO_ECHO'] || false opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil? opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil? opts[:drain_timeout] ||= ENV['NATS_DRAIN_TIMEOUT'].to_i unless ENV['NATS_DRAIN_TIMEOUT'].nil? uri = opts[:uris] || opts[:servers] || opts[:uri] if opts[:tls] case when opts[:tls][:ca_file] # Ensure that the file exists before going further # in order to report configuration errors during # connect synchronously. if !File.readable?(opts[:tls][:ca_file]) raise(Error, "TLS Verification is enabled but ca_file %s is not readable" % opts[:tls][:ca_file]) end # Certificate is supplied so assume we mean verification by default, # but still allow disabling explicitly by setting to false. opts[:tls][:verify_peer] ||= true when (opts[:tls][:verify_peer] && !opts[:tls][:ca_file]) raise(Error, "TLS Verification is enabled but ca_file is not set") else # Otherwise, disable verifying peer by default, # thus never reaching EM#ssl_verify_peer opts[:tls][:verify_peer] = false end # Allow overriding directly but default to those which server supports. opts[:tls][:ssl_version] ||= %w(tlsv1 tlsv1_1 tlsv1_2) opts[:tls][:protocols] ||= %w(tlsv1 tlsv1_1 tlsv1_2) end # If they pass an array here just pass along to the real connection, and use first as the first attempt.. # Real connection will do proper walk throughs etc.. unless uri.nil? uris = uri.kind_of?(Array) ? uri : [uri] uris.shuffle! unless opts[:dont_randomize_servers] u = uris.first @uri = u.is_a?(URI) ? u.dup : URI.parse(u) end @err_cb = proc { |e| raise e } unless err_cb @close_cb = proc { } unless close_cb @disconnect_cb = proc { } unless disconnect_cb client = EM.connect(@uri.host, @uri.port, self, opts) client.on_connect(&blk) if blk return client end # Create a default client connection to the server. # @see NATS::connect def start(*args, &blk) @reactor_was_running = EM.reactor_running? unless (@reactor_was_running || blk) raise(Error, "EM needs to be running when NATS.start is called without a run block") end # Setup optimized select versions if EM.epoll? EM.epoll elsif EM.kqueue? EM.kqueue elsif EM.library_type == :java # No warning needed, we're using Java NIO else Kernel.warn('Neither epoll nor kqueue are supported, performance may be impacted') end EM.run { @client = connect(*args, &blk) } end # Close the default client connection and optionally call the associated block. # @param [Block] &blk called when the connection is closed. def stop(&blk) client.close if (client and (client.connected? || client.reconnecting?)) blk.call if blk @err_cb = nil @close_cb = nil @reconnect_cb = nil @disconnect_cb = nil end # Drain gracefully disconnects from the server, letting # subscribers process pending messages already sent by server and # optionally calls the associated block. # @param [Block] &blk called when drain is done and connection is closed. def drain(&blk) if (client and !client.draining? and (client.connected? || client.reconnecting?)) client.drain { blk.call if blk } end end # @return [URI] Connected server def connected_server return nil unless client client.connected_server end # @return [Boolean] Connected state def connected? return false unless client client.connected? end # @return [Boolean] Reconnecting state def reconnecting? return false unless client client.reconnecting? end # @return [Boolean] Draining state def draining? return false unless client client.draining? end # @return [Hash] Options def options return {} unless client client.options end # @return [Hash] Server information def server_info return nil unless client client.server_info end # Set the default on_error callback. # @param [Block] &callback called when an error has been detected. def on_error(&callback) @err_cb, @err_cb_overridden = callback, true end # Set the default on_reconnect callback. # @param [Block] &callback called when a reconnect attempt is made. def on_reconnect(&callback) @reconnect_cb = callback @client.on_reconnect(&callback) unless @client.nil? end # Set the default on_disconnect callback. # @param [Block] &callback called whenever client disconnects from a server. def on_disconnect(&callback) @disconnect_cb = callback @client.on_disconnect(&callback) unless @client.nil? end # Set the default on_closed callback. # @param [Block] &callback called when will reach a state when will no longer be connected. def on_close(&callback) @close_cb = callback @client.on_close(&callback) unless @client.nil? end # Publish a message using the default client connection. # @see NATS#publish def publish(*args, &blk) (@client ||= connect).publish(*args, &blk) end # Subscribe using the default client connection. # @see NATS#subscribe def subscribe(*args, &blk) (@client ||= connect).subscribe(*args, &blk) end # Cancel a subscription on the default client connection. # @see NATS#unsubscribe def unsubscribe(*args) (@client ||= connect).unsubscribe(*args) end # Set a timeout for receiving messages for the subscription. # @see NATS#timeout def timeout(*args, &blk) (@client ||= connect).timeout(*args, &blk) end # Publish a message and wait for a response on the default client connection. # @see NATS#request def request(*args, &blk) (@client ||= connect).request(*args, &blk) end # Returns a subject that can be used for "directed" communications. # @return [String] def create_inbox "_INBOX.#{SecureRandom.hex(13)}" end # Flushes all messages and subscriptions in the default connection # @see NATS#flush def flush(*args, &blk) (@client ||= connect).flush(*args, &blk) end # Return bytes outstanding for the default client connection. # @see NATS#pending_data_size def pending_data_size(*args) (@client ||= connect).pending_data_size(*args) end def wait_for_server(uri, max_wait = 5) # :nodoc: start = Time.now while (Time.now - start < max_wait) # Wait max_wait seconds max break if server_running?(uri) sleep(0.1) end end def server_running?(uri) # :nodoc: require 'socket' s = TCPSocket.new(uri.host, uri.port) s.close return true rescue return false end def clear_client # :nodoc: @client = nil end private def uri_is_remote?(uri) uri.host != 'localhost' && uri.host != '127.0.0.1' end def process_uri(uris) connect_uris = [] uris.split(',').each do |uri| opts = {} # Scheme if uri.include?("://") scheme, uri = uri.split("://") opts[:scheme] = scheme else opts[:scheme] = 'nats' end # UserInfo if uri.include?("@") userinfo, endpoint = uri.split("@") host, port = endpoint.split(":") opts[:userinfo] = userinfo else host, port = uri.split(":") end # Host and Port opts[:host] = host || "localhost" opts[:port] = port || DEFAULT_PORT connect_uris << URI::Generic.build(opts) end connect_uris end end attr_reader :connected, :connect_cb, :err_cb, :err_cb_overridden, :pongs_received #:nodoc: attr_reader :closing, :reconnecting, :draining, :server_pool, :options, :server_info #:nodoc attr_reader :msgs_received, :msgs_sent, :bytes_received, :bytes_sent, :pings attr_reader :disconnect_cb, :close_cb alias :connected? :connected alias :closing? :closing alias :reconnecting? :reconnecting alias :draining? :draining def initialize(options) @options = options process_uri_options @buf = nil @ssid, @subs = 1, {} @err_cb = NATS.err_cb @close_cb = NATS.close_cb @reconnect_cb = NATS.reconnect_cb @disconnect_cb = NATS.disconnect_cb @reconnect_timer, @needed = nil, nil @connected, @closing, @reconnecting, @conn_cb_called = false, false, false, false @msgs_received = @msgs_sent = @bytes_received = @bytes_sent = @pings = 0 @pending_size = 0 @server_info = { } # Mark whether we should be connecting securely, try best effort # in being compatible with present ssl support. @ssl = false @tls = nil @tls = options[:tls] if options[:tls] @ssl = options[:ssl] if options[:ssl] or @tls # New style request/response implementation. @resp_sub = nil @resp_map = nil @resp_sub_prefix = nil @nuid = NATS::NUID.new # Drain mode @draining = false @drained_subs = false # NKEYS @user_credentials = options[:user_credentials] if options[:user_credentials] @nkeys_seed = options[:nkeys_seed] if options[:nkeys_seed] @user_nkey_cb = nil @user_jwt_cb = nil @signature_cb = nil # NKEYS setup_nkeys_connect if @user_credentials or @nkeys_seed end # Publish a message to a given subject, with optional reply subject and completion block # @param [String] subject # @param [Object, #to_s] msg # @param [String] opt_reply # @param [Block] blk, closure called when publish has been processed by the server. def publish(subject, msg=EMPTY_MSG, opt_reply=nil, &blk) return unless subject and not @drained_subs msg = msg.to_s # Accounting @msgs_sent += 1 @bytes_sent += msg.bytesize if msg send_command("PUB #{subject} #{opt_reply} #{msg.bytesize}#{CR_LF}#{msg}#{CR_LF}") queue_server_rt(&blk) if blk end # Subscribe to a subject with optional wildcards. # Messages will be delivered to the supplied callback. # Callback can take any number of the supplied arguments as defined by the list: msg, reply, sub. # Returns subscription id which can be passed to #unsubscribe. # @param [String] subject, optionally with wilcards. # @param [Hash] opts, optional options hash, e.g. :queue, :max. # @param [Block] callback, called when a message is delivered. # @return [Object] sid, Subject Identifier def subscribe(subject, opts={}, &callback) return unless subject and not draining? sid = (@ssid += 1) sub = @subs[sid] = { :subject => subject, :callback => callback, :received => 0 } sub[:queue] = opts[:queue] if opts[:queue] sub[:max] = opts[:max] if opts[:max] send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}") # Setup server support for auto-unsubscribe unsubscribe(sid, opts[:max]) if opts[:max] sid end # Cancel a subscription. # @param [Object] sid # @param [Number] opt_max, optional number of responses to receive before auto-unsubscribing def unsubscribe(sid, opt_max=nil) return if draining? opt_max_str = " #{opt_max}" unless opt_max.nil? send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}") return unless sub = @subs[sid] sub[:max] = opt_max @subs.delete(sid) unless (sub[:max] && (sub[:received] < sub[:max])) end # Drain gracefully closes the connection. # @param [Block] blk called when drain is done and connection is closed. def drain(&blk) return if draining? or closing? @draining = true # Remove interest in all subjects to stop receiving messages. @subs.each do |sid, _| send_command("UNSUB #{sid} #{CR_LF}") end # Roundtrip to ensure no more messages are received. flush do drain_timeout_timer, draining_timer = nil, nil drain_timeout_timer = EM.add_timer(options[:drain_timeout]) do EM.cancel_timer(draining_timer) # Report the timeout via the error callback and just close err_cb.call(NATS::ClientError.new("Drain Timeout")) @draining = false close unless closing? blk.call if blk end # Periodically check for the pending data to be empty. draining_timer = EM.add_periodic_timer(0.1) do next unless closing? or @buf.nil? or @buf.empty? # Subscriptions have been drained already so disallow publishing. @drained_subs = true next unless pending_data_size == 0 EM.cancel_timer(draining_timer) EM.cancel_timer(drain_timeout_timer) # We're done draining and can close now. @draining = false close unless closing? blk.call if blk end end end # Return the active subscription count. # @return [Number] def subscription_count @subs.size end # Setup a timeout for receiving messages for the subscription. # @param [Object] sid # @param [Number] timeout, float in seconds # @param [Hash] opts, options, :auto_unsubscribe(true), :expected(1) def timeout(sid, timeout, opts={}, &callback) # Setup a timeout if requested return unless sub = @subs[sid] auto_unsubscribe, expected = true, 1 auto_unsubscribe = opts[:auto_unsubscribe] if opts.key?(:auto_unsubscribe) expected = opts[:expected] if opts.key?(:expected) EM.cancel_timer(sub[:timeout]) if sub[:timeout] sub[:timeout] = EM.add_timer(timeout) do unsubscribe(sid) if auto_unsubscribe callback.call(sid) if callback end sub[:expected] = expected end # Send a request and have the response delivered to the supplied callback. # @param [String] subject # @param [Object] msg # @param [Block] callback # @return [Object] sid def request(subject, data=nil, opts={}, &cb) return unless subject # In case of using async request then fallback to auto unsubscribe # based request/response and not break compatibility too much since # new request/response style can only be used with fibers. if cb inbox = "_INBOX.#{@nuid.next}" s = subscribe(inbox, opts) { |msg, reply| case cb.arity when 0 then cb.call when 1 then cb.call(msg) else cb.call(msg, reply) end } publish(subject, data, inbox) return s end # If this is the first request being made, then need to start # the responses mux handler that handles the responses. start_resp_mux_sub! unless @resp_sub_prefix # Generate unique token for the reply subject. token = @nuid.next inbox = "#{@resp_sub_prefix}.#{token}" # Synchronous request/response requires using a Fiber # to be able to await the response. f = Fiber.current @resp_map[token][:fiber] = f # If awaiting more than a single response then use array # to include all that could be gathered before the deadline. expected = opts[:max] ||= 1 @resp_map[token][:expected] = expected @resp_map[token][:msgs] = [] if expected > 1 # Announce the request with the inbox using the token. publish(subject, data, inbox) # If deadline expires, then discard the token and resume fiber opts[:timeout] ||= 0.5 t = EM.add_timer(opts[:timeout]) do if expected > 1 f.resume @resp_map[token][:msgs] else f.resume end @resp_map.delete(token) end # Wait for the response and cancel timeout callback if received. if expected > 1 # Wait to receive all replies that can get before deadline. msgs = Fiber.yield EM.cancel_timer(t) # Slice and throwaway responses that are not needed. return msgs.slice(0, expected) else msg = Fiber.yield EM.cancel_timer(t) return msg end end def start_resp_mux_sub! @resp_sub_prefix = "_INBOX.#{@nuid.next}" @resp_map = Hash.new { |h,k| h[k] = { }} # Single subscription that will be handling all the requests # using fibers to yield the responses. subscribe("#{@resp_sub_prefix}.*") do |msg, reply, subject| token = subject.split('.').last # Discard the response if requestor not interested already. next unless @resp_map.key? token # Take fiber that will be passed the response f = @resp_map[token][:fiber] expected = @resp_map[token][:expected] if expected == 1 f.resume msg @resp_map.delete(token) next end if @resp_map[token][:msgs].size < expected @resp_map[token][:msgs] << msg msgs = @resp_map[token][:msgs] if msgs.size >= expected f.resume(msgs) else # Wait to gather more messages or timeout. next end end @resp_map.delete(token) end end # Flushes all messages and subscriptions for the connection. # All messages and subscriptions have been processed by the server # when the optional callback is called. def flush(&blk) queue_server_rt(&blk) if blk end # Define a callback to be called when the client connection has been established. # @param [Block] callback def on_connect(&callback) @connect_cb = callback end # Define a callback to be called when errors occur on the client connection. # @param [Block] &callback called when an error has been detected. def on_error(&callback) @err_cb, @err_cb_overridden = callback, true end # Define a callback to be called when a reconnect attempt is made. # @param [Block] &callback called when a reconnect attempt is made. def on_reconnect(&callback) @reconnect_cb = callback end # Define a callback to be called when client is disconnected from server. # @param [Block] &callback called whenever client disconnects from a server. def on_disconnect(&callback) @disconnect_cb = callback end # Define a callback to be called when client is disconnected from server. # @param [Block] &callback called when will reach a state when will no longer be connected. def on_close(&callback) @close_cb = callback end # Close the connection to the server. def close @closing = true cancel_ping_timer cancel_reconnect_timer close_connection_after_writing if connected? process_disconnect if reconnecting? end # Return bytes outstanding waiting to be sent to server. def pending_data_size get_outbound_data_size + @pending_size end # Return snapshot of current traffic flow stats in the client. def stats { in_msgs: @msgs_received, out_msgs: @msgs_sent, in_bytes: @bytes_received, out_bytes: @bytes_sent }.freeze end def user_err_cb? # :nodoc: err_cb_overridden || NATS.err_cb_overridden end def auth_connection? !@uri.user.nil? || @options[:token] || @server_info[:auth_required] end def connect_command #:nodoc: cs = { :verbose => @options[:verbose], :pedantic => @options[:pedantic], :lang => ::NATS::LANG, :version => ::NATS::VERSION, :protocol => ::NATS::PROTOCOL_VERSION, :echo => !@options[:no_echo] } case when @options[:user_credentials] nonce = @server_info[:nonce] cs[:jwt] = @user_jwt_cb.call cs[:sig] = @signature_cb.call(nonce) when @options[:nkeys_seed] nonce = @server_info[:nonce] cs[:nkey] = @user_nkey_cb.call cs[:sig] = @signature_cb.call(nonce) when @options[:token] cs[:auth_token] = @options[:token] when @uri.password.nil? cs[:auth_token] = @uri.user else cs[:user] = @uri.user cs[:pass] = @uri.password end if auth_connection? cs[:name] = @options[:name] if @options[:name] cs[:ssl_required] = @ssl if @ssl cs[:tls_required] = true if @tls "CONNECT #{cs.to_json}#{CR_LF}" end def send_connect_command #:nodoc: send_command(connect_command, true) end def queue_server_rt(&cb) #:nodoc: return unless cb (@pongs ||= []) << cb send_command(PING_REQUEST) end def on_msg(subject, sid, reply, msg) #:nodoc: # Accounting - We should account for inbound even if they are not processed. @msgs_received += 1 @bytes_received += msg.bytesize if msg return unless sub = @subs[sid] # Check for auto_unsubscribe sub[:received] += 1 if sub[:max] # Client side support in case server did not receive unsubscribe return unsubscribe(sid) if (sub[:received] > sub[:max]) # cleanup here if we have hit the max.. @subs.delete(sid) if (sub[:received] == sub[:max]) end if cb = sub[:callback] case cb.arity when 0 then cb.call when 1 then cb.call(msg) when 2 then cb.call(msg, reply) else cb.call(msg, reply, subject) end end # Check for a timeout, and cancel if received >= expected if (sub[:timeout] && sub[:received] >= sub[:expected]) EM.cancel_timer(sub[:timeout]) sub[:timeout] = nil end end def flush_pending #:nodoc: return unless @pending send_data(@pending.join) @pending, @pending_size = nil, 0 end def receive_data(data) #:nodoc: @buf = @buf ? @buf << data : data while (@buf) case @parse_state when AWAITING_INFO_LINE case @buf when INFO @buf = $' process_connect_init($1) else # If we are here we do not have a complete line yet that we understand. return end when AWAITING_CONTROL_LINE case @buf when MSG @buf = $' @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i @parse_state = AWAITING_MSG_PAYLOAD when OK # No-op right now @buf = $' when ERR @buf = $' current = server_pool.first current[:error_received] = true if current[:auth_required] && !current[:auth_ok] err_cb.call(NATS::AuthError.new($1)) else err_cb.call(NATS::ServerError.new($1)) end when PING @pings += 1 @buf = $' send_command(PONG_RESPONSE) when PONG @buf = $' cb = @pongs.shift cb.call if cb when INFO @buf = $' process_info($1) when UNKNOWN @buf = $' err_cb.call(NATS::ServerError.new("Unknown protocol: #{$1}")) else # If we are here we do not have a complete line yet that we understand. return end @buf = nil if (@buf && @buf.empty?) when AWAITING_MSG_PAYLOAD return unless (@needed && @buf.bytesize >= (@needed + CR_LF_SIZE)) on_msg(@sub, @sid, @reply, @buf.slice(0, @needed)) @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize) @sub = @sid = @reply = @needed = nil @parse_state = AWAITING_CONTROL_LINE @buf = nil if (@buf && @buf.empty?) end end end def process_connect_init(info) # :nodoc: # Each JSON parser uses a different key/value pair to use symbol keys # instead of strings when parsing. Passing all three pairs assures each # parser gets what it needs. For the json gem :symbolize_name, for yajl # :symbolize_keys, and for oj :symbol_keys. @server_info = JSON.parse(info, :symbolize_keys => true, :symbolize_names => true, :symbol_keys => true) case when (server_using_secure_connection? and client_using_secure_connection?) # Allow parameterizing secure connection via EM#start_tls directly if present. start_tls(@tls || {}) when (server_using_secure_connection? and !client_using_secure_connection?) # Call unbind since there is a configuration mismatch between client/server # anyway and communication cannot happen in this state. err_cb.call(NATS::ClientError.new('TLS/SSL required by server')) close_connection_after_writing when (client_using_secure_connection? and !server_using_secure_connection?) err_cb.call(NATS::ClientError.new('TLS/SSL not supported by server')) close_connection_after_writing else # Otherwise, use a regular connection. end # Check whether there no echo is supported by the server. if @options[:no_echo] if @server_info[:proto].nil? || @server_info[:proto] < 1 err_cb.call(NATS::ServerError.new('No echo option not supported by this server')) close_connection_after_writing end end send_connect_command # Only initial INFO command is treated specially for auth reasons, # the rest are processed asynchronously to discover servers. @parse_state = AWAITING_CONTROL_LINE process_info(info) process_connect if @server_info[:auth_required] current = server_pool.first current[:auth_required] = true # Send pending connect followed by ping/pong to ensure we're authorized. queue_server_rt { current[:auth_ok] = true } end flush_pending end def process_info(info_line) #:nodoc: info = JSON.parse(info_line, :symbolize_keys => true, :symbolize_names => true, :symbol_keys => true) # Detect any announced server that we might not be aware of... connect_urls = info[:connect_urls] if connect_urls srvs = [] connect_urls.each do |url| u = URI.parse("nats://#{url}") present = server_pool.detect do |srv| srv[:uri].host == u.host && srv[:uri].port == u.port end if not present # Let explicit user and pass options set the credentials. u.user = options[:user] if options[:user] u.password = options[:pass] if options[:pass] # Use creds from the current server if not set explicitly. if @uri and !@uri.user.nil? and !@uri.user.empty? u.user ||= @uri.user u.password ||= @uri.password end srvs << { :uri => u, :reconnect_attempts => 0, :discovered => true } end end srvs.shuffle! unless @options[:dont_randomize_servers] # Include in server pool but keep current one as the first one. server_pool.push(*srvs) end info end def client_using_secure_connection? @tls || @ssl end def server_using_secure_connection? @server_info[:ssl_required] || @server_info[:tls_required] end def ssl_verify_peer(cert) incoming = OpenSSL::X509::Certificate.new(cert) store = OpenSSL::X509::Store.new store.set_default_paths store.add_file @options[:tls][:ca_file] result = store.verify(incoming) err_cb.call(NATS::ConnectError.new('TLS Verification failed checking issuer based on CA %s' % @options[:tls][:ca_file])) unless result result rescue NATS::ConnectError false end def cancel_ping_timer if @ping_timer EM.cancel_timer(@ping_timer) @ping_timer = nil end end def connection_completed #:nodoc: @parse_state = AWAITING_INFO_LINE # Delay sending CONNECT or any other command here until we are sure # that we have a valid established secure connection. return if (@ssl or @tls) # Mark that we established already TCP connection to the server, # when using TLS we only do so after handshake has been completed. @connected = true end def ssl_handshake_completed @connected = true end def process_connect #:nodoc: # Reset reconnect attempts since TCP connection has been successful at this point. current = server_pool.first current[:was_connected] = true current[:reconnect_attempts] ||= 0 cancel_reconnect_timer if reconnecting? # Whip through any pending SUB commands since we replay # all subscriptions already done anyway. @pending.delete_if { |sub| sub[0..2] == SUB_OP } if @pending @subs.each_pair { |k, v| send_command("SUB #{v[:subject]} #{v[:queue]} #{k}#{CR_LF}") } unless user_err_cb? or reconnecting? @err_cb = proc { |e| raise e } end # We have validated the connection at this point so send CONNECT # and any other pending commands which we need to the server. flush_pending if (connect_cb and not @conn_cb_called) # We will round trip the server here to make sure all state from any pending commands # has been processed before calling the connect callback. queue_server_rt do connect_cb.call(self) @conn_cb_called = true end end # Notify via reconnect callback that we are again plugged again into the system. if reconnecting? @reconnecting = false @reconnect_cb.call(self) unless @reconnect_cb.nil? end # Initialize ping timer and processing @pings_outstanding = 0 @pongs_received = 0 @ping_timer = EM.add_periodic_timer(@options[:ping_interval]) do send_ping end end def send_ping #:nodoc: return if @closing @pings_outstanding += 1 if @pings_outstanding > @options[:max_outstanding_pings] close_connection #close return end queue_server_rt { process_pong } flush_pending end def process_pong @pongs_received += 1 @pings_outstanding -= 1 end def should_delay_connect?(server) case when server[:was_connected] server[:reconnect_attempts] >= 0 when server[:last_reconnect_attempt] (MonotonicTime.now - server[:last_reconnect_attempt]) < @options[:reconnect_time_wait] else false end end def schedule_reconnect #:nodoc: @reconnecting = true @connected = false @reconnect_timer = EM.add_timer(@options[:reconnect_time_wait]) { attempt_reconnect } end def unbind #:nodoc: # Allow notifying from which server we were disconnected, # but only when we didn't trigger disconnecting ourselves. if @disconnect_cb and connected? and not closing? @disconnect_cb.call(NATS::ConnectError.new(disconnect_error_string)) end # If we are closing or shouldn't reconnect, go ahead and disconnect. process_disconnect and return if (closing? or should_not_reconnect?) @reconnecting = true if connected? @connected = false @pending = @pongs = nil @buf = nil cancel_ping_timer schedule_primary_and_connect end def multiple_servers_available? server_pool && server_pool.size > 1 end def had_error? server_pool.first && server_pool.first[:error_received] end def should_not_reconnect? !@options[:reconnect] end def cancel_reconnect_timer if @reconnect_timer EM.cancel_timer(@reconnect_timer) @reconnect_timer = nil end end def disconnect_error_string return "Client disconnected from server on #{@uri}" if @connected return "Could not connect to server on #{@uri}" end def process_disconnect #:nodoc: # Mute error callback when user has called NATS.close on purpose. if not closing? and @err_cb # Always call error callback for compatibility with previous behavior. err_cb.call(NATS::ConnectError.new(disconnect_error_string)) end close_cb.call if @close_cb true # Chaining ensure cancel_ping_timer cancel_reconnect_timer if (NATS.client == self) NATS.clear_client EM.stop if ((connected? || reconnecting?) and closing? and not NATS.reactor_was_running?) end @connected = @reconnecting = false end def can_reuse_server?(server) #:nodoc: # If we will retry a number of times to reconnect to a server # unless we got an error from it already. reconnecting? && server[:reconnect_attempts] <= @options[:max_reconnect_attempts] && !server[:error_received] end def attempt_reconnect #:nodoc: @reconnect_timer = nil current = server_pool.first # Snapshot time when trying to reconnect to server # in order to back off for subsequent attempts. current[:last_reconnect_attempt] = MonotonicTime.now current[:reconnect_attempts] ||= 0 current[:reconnect_attempts] += 1 begin EM.reconnect(@uri.host, @uri.port, self) rescue current[:error_received] = true @uri = nil @connected = false end end def send_command(command, priority = false) #:nodoc: needs_flush = (connected? && @pending.nil?) @pending ||= [] @pending << command unless priority @pending.unshift(command) if priority @pending_size += command.bytesize EM.next_tick { flush_pending } if needs_flush flush_pending if (connected? && @pending_size > MAX_PENDING_SIZE) if (@options[:fast_producer_error] && pending_data_size > FAST_PRODUCER_THRESHOLD) err_cb.call(NATS::ClientError.new("Fast Producer: #{pending_data_size} bytes outstanding")) end true end def setup_nkeys_connect begin require 'nkeys' require 'base64' rescue LoadError raise(Error, "nkeys is not installed") end case when @nkeys_seed @user_nkey_cb = proc { seed = File.read(@nkeys_seed).chomp kp = NKEYS::from_seed(seed) # Take a copy since original will be gone with the wipe. pub_key = kp.public_key.dup kp.wipe! pub_key } @signature_cb = proc { |nonce| seed = File.read(@nkeys_seed).chomp kp = NKEYS::from_seed(seed) raw_signed = kp.sign(nonce) kp.wipe! encoded = Base64.urlsafe_encode64(raw_signed) encoded.gsub('=', '') } when @user_credentials # When the credentials are within a single decorated file. @user_jwt_cb = proc { jwt_start = "BEGIN NATS USER JWT".freeze found = false jwt = nil File.readlines(@user_credentials).each do |line| case when found jwt = line.chomp break when line.include?(jwt_start) found = true end end raise(Error, "No JWT found in #{@user_credentials}") if not found jwt } @signature_cb = proc { |nonce| seed_start = "BEGIN USER NKEY SEED".freeze found = false seed = nil File.readlines(@user_credentials).each do |line| case when found seed = line.chomp break when line.include?(seed_start) found = true end end raise(Error, "No nkey user seed found in #{@user_credentials}") if not found kp = NKEYS::from_seed(seed) raw_signed = kp.sign(nonce) # seed is a reference so also cleared when doing wipe, # which can be done since Ruby strings are mutable. kp.wipe encoded = Base64.urlsafe_encode64(raw_signed) # Remove padding encoded.gsub('=', '') } end end # Parse out URIs which can now be an array of server choices # The server pool will contain both explicit and implicit members. def process_uri_options #:nodoc @server_pool = [] uri = options[:uris] || options[:servers] || options[:uri] uri = uri.kind_of?(Array) ? uri : [uri] uri.each { |u| server_pool << { :uri => u.is_a?(URI) ? u.dup : URI.parse(u) } } bind_primary end # @return [URI] Connected server def connected_server connected? ? @uri : nil end # Retrieves the list of servers which have been discovered # via server connect_urls announcements def discovered_servers server_pool.select {|s| s[:discovered] } end def bind_primary #:nodoc: first = server_pool.first @uri = first[:uri] @uri.user = options[:user] if options[:user] @uri.password = options[:pass] if options[:pass] first end # We have failed on an attempt at the primary (first) server, rotate and try again def schedule_primary_and_connect #:nodoc: # Dump the one we were trying if it wasn't connected current = server_pool.shift # In case there was an error from the server we will take it out from rotation # unless we specify infinite reconnects via setting :max_reconnect_attempts to -1 if current && (options[:max_reconnect_attempts] < 0 || can_reuse_server?(current)) server_pool << current end # If we are out of options, go ahead and disconnect then # handle closing connection to NATS. process_disconnect and return if server_pool.empty? # bind new one next_server = bind_primary # If the next one was connected and we are trying to reconnect # set up timer if we tried once already. if should_delay_connect?(next_server) schedule_reconnect else attempt_reconnect schedule_primary_and_connect if had_error? end end def inspect #:nodoc: "" end class MonotonicTime class << self case when defined?(Process::CLOCK_MONOTONIC) def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end when RUBY_ENGINE == 'jruby' def now java.lang.System.nanoTime() / 1_000_000_000.0 end else def now # Fallback to regular time behavior ::Time.now.to_f end end end end end ================================================ FILE: lib/nats/ext/bytesize.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # if RUBY_VERSION <= "1.8.6" class String #:nodoc: def bytesize; self.size; end end end ================================================ FILE: lib/nats/ext/em.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # begin require 'eventmachine' rescue LoadError require 'rubygems' require 'eventmachine' end # Check for get_outbound_data_size support, fake it out if it doesn't exist, e.g. jruby if !EM::Connection.method_defined? :get_outbound_data_size class EM::Connection def get_outbound_data_size; return 0; end end end ================================================ FILE: lib/nats/ext/json.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # begin require 'yajl' require 'yajl/json_gem' rescue LoadError begin require 'oj' Oj.mimic_JSON() rescue LoadError require 'rubygems' require 'json' end end ================================================ FILE: lib/nats/nuid.rb ================================================ # Copyright 2016-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'securerandom' module NATS class NUID DIGITS = [*'0'..'9', *'A'..'Z', *'a'..'z'] BASE = 62 PREFIX_LENGTH = 12 SEQ_LENGTH = 10 TOTAL_LENGTH = PREFIX_LENGTH + SEQ_LENGTH MAX_SEQ = BASE**10 MIN_INC = 33 MAX_INC = 333 INC = MAX_INC - MIN_INC def initialize @prand = Random.new @seq = @prand.rand(MAX_SEQ) @inc = MIN_INC + @prand.rand(INC) @prefix = '' randomize_prefix! end def next @seq += @inc if @seq >= MAX_SEQ randomize_prefix! reset_sequential! end l = @seq # Do this inline 10 times to avoid even more extra allocs, # then use string interpolation of everything which works # faster for doing concat. s_10 = DIGITS[l % BASE]; # Ugly, but parallel assignment is slightly faster here... s_09, s_08, s_07, s_06, s_05, s_04, s_03, s_02, s_01 = \ (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\ (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\ (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]) "#{@prefix}#{s_01}#{s_02}#{s_03}#{s_04}#{s_05}#{s_06}#{s_07}#{s_08}#{s_09}#{s_10}" end def randomize_prefix! @prefix = \ SecureRandom.random_bytes(PREFIX_LENGTH).each_byte .reduce('') do |prefix, n| prefix << DIGITS[n % BASE] end end private def reset_sequential! @seq = @prand.rand(MAX_SEQ) @inc = MIN_INC + @prand.rand(INC) end end end ================================================ FILE: lib/nats/server/cluster.rb ================================================ require 'uri' module NATSD #:nodoc: all class Server class << self attr_reader :opt_routes, :route_auth_required, :route_ssl_required, :reconnect_interval attr_accessor :num_routes alias route_auth_required? :route_auth_required alias route_ssl_required? :route_ssl_required def connected_routes @routes ||= [] end def add_route(route) connected_routes << route unless route.nil? end def remove_route(route) connected_routes.delete(route) unless route.nil? end def route_info_string @route_info = { :server_id => Server.id, :host => @options[:cluster_net] || host, :port => @options[:cluster_port], :version => VERSION, :auth_required => route_auth_required?, :ssl_required => false, # FIXME! :max_payload => @max_payload } @route_info.to_json end def route_key(route_url) r = URI.parse(route_url) "#{r.host}:#{r.port}" end def route_auth_ok?(user, pass) user == @options[:cluster_user] && pass == @options[:cluster_pass] end def solicit_routes #:nodoc: @opt_routes = [] NATSD::Server.options[:cluster_routes].each do |r_url| opt_routes << { :route => r_url, :uri => URI.parse(r_url), :key => route_key(r_url) } end try_to_connect_routes end def try_to_connect_routes #:nodoc: opt_routes.each do |route| # FIXME, Strip auth debug "Trying to connect to route: #{route[:route]}" EM.connect(route[:uri].host, route[:uri].port, NATSD::Route, route) end end def broadcast_proto_to_routes(proto) connected_routes.each { |r| r.queue_data(proto) } end def rsid_qsub(rsid) cid, sid = parse_rsid(rsid) conn = Server.connections[cid] sub = conn.subscriptions[sid] sub if sub.qgroup rescue nil end def parse_rsid(rsid) m = RSID.match(rsid) return [m[1].to_i, m[2]] if m end def routed_sid(sub) "RSID:#{sub.conn.cid}:#{sub.sid}" end def route_sub_proto(sub) return "SUB #{sub.subject} #{routed_sid(sub)}#{CR_LF}" if sub.qgroup.nil? return "SUB #{sub.subject} #{sub.qgroup} #{routed_sid(sub)}#{CR_LF}" end def broadcast_sub_to_routes(sub) broadcast_proto_to_routes(route_sub_proto(sub)) end def broadcast_unsub_to_routes(sub) opt_max_str = " #{sub.max_responses}" unless sub.max_responses.nil? broadcast_proto_to_routes("UNSUB #{routed_sid(sub)}#{opt_max_str}#{CR_LF}") end end end end ================================================ FILE: lib/nats/server/connection.rb ================================================ module NATSD #:nodoc: all module Connection #:nodoc: all attr_accessor :in_msgs, :out_msgs, :in_bytes, :out_bytes attr_reader :cid, :closing, :last_activity, :writev_size, :subscriptions alias :closing? :closing def flush_data return if @writev.nil? || closing? send_data(@writev.join) @writev, @writev_size = nil, 0 end def queue_data(data) EM.next_tick { flush_data } if @writev.nil? (@writev ||= []) << data @writev_size += data.bytesize flush_data if @writev_size > MAX_WRITEV_SIZE end def client_info cur_peername = get_peername @client_info ||= (cur_peername.nil? ? 'N/A' : Socket.unpack_sockaddr_in(cur_peername)) end def info { :cid => cid, :ip => client_info[1], :port => client_info[0], :subscriptions => @subscriptions.size, :pending_size => get_outbound_data_size, :in_msgs => @in_msgs, :out_msgs => @out_msgs, :in_bytes => @in_bytes, :out_bytes => @out_bytes } end def max_connections_exceeded? return false unless (Server.num_connections > Server.max_connections) error_close MAX_CONNS_EXCEEDED debug "Maximum #{Server.max_connections} connections exceeded, c:#{cid} will be closed" true end def post_init @cid = Server.cid @subscriptions = {} @verbose = @pedantic = true # suppressed by most clients, but allows friendly telnet @in_msgs = @out_msgs = @in_bytes = @out_bytes = 0 @writev_size = 0 @parse_state = AWAITING_CONTROL_LINE send_info debug "#{type} connection created", client_info, cid if Server.ssl_required? debug "Starting TLS/SSL", client_info, cid flush_data @ssl_pending = EM.add_timer(NATSD::Server.ssl_timeout) { connect_ssl_timeout } start_tls(:verify_peer => true) if Server.ssl_required? end @auth_pending = EM.add_timer(NATSD::Server.auth_timeout) { connect_auth_timeout } if Server.auth_required? @ping_timer = EM.add_periodic_timer(NATSD::Server.ping_interval) { send_ping } @pings_outstanding = 0 inc_connections return if max_connections_exceeded? end def send_ping return if @closing if @pings_outstanding > NATSD::Server.ping_max error_close UNRESPONSIVE return end queue_data(PING_RESPONSE) flush_data @pings_outstanding += 1 end def connect_auth_timeout error_close AUTH_REQUIRED debug "#{type} connection timeout due to lack of auth credentials", cid end def connect_ssl_timeout error_close SSL_REQUIRED debug "#{type} connection timeout due to lack of TLS/SSL negotiations", cid end def receive_data(data) @buf = @buf ? @buf << data : data while (@buf && !@closing) case @parse_state when AWAITING_CONTROL_LINE case @buf when PUB_OP ctrace('PUB OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' @parse_state = AWAITING_MSG_PAYLOAD @msg_sub, @msg_reply, @msg_size = $1, $3, $4.to_i if (@msg_size > NATSD::Server.max_payload) debug_print_msg_too_big(@msg_size) error_close PAYLOAD_TOO_BIG end queue_data(INVALID_SUBJECT) if (@pedantic && !(@msg_sub =~ SUB_NO_WC)) when SUB_OP ctrace('SUB OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' sub, qgroup, sid = $1, $3, $4 return queue_data(INVALID_SUBJECT) if !($1 =~ SUB) return queue_data(INVALID_SID_TAKEN) if @subscriptions[sid] sub = Subscriber.new(self, sub, sid, qgroup, 0) @subscriptions[sid] = sub Server.subscribe(sub) queue_data(OK) if @verbose when UNSUB_OP ctrace('UNSUB OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' sid, sub = $1, @subscriptions[$1] if sub # If we have set max_responses, we will unsubscribe once we have received # the appropriate amount of responses. sub.max_responses = ($2 && $3) ? $3.to_i : nil delete_subscriber(sub) unless (sub.max_responses && (sub.num_responses < sub.max_responses)) queue_data(OK) if @verbose else queue_data(INVALID_SID_NOEXIST) if @pedantic end when PING ctrace('PING OP') if NATSD::Server.trace_flag? @buf = $' queue_data(PONG_RESPONSE) flush_data when PONG ctrace('PONG OP') if NATSD::Server.trace_flag? @buf = $' @pings_outstanding -= 1 when CONNECT ctrace('CONNECT OP', strip_op($&)) if NATSD::Server.trace_flag? @buf = $' begin config = JSON.parse($1) process_connect_config(config) rescue => e queue_data(INVALID_CONFIG) log_error end when INFO_REQ ctrace('INFO_REQUEST OP') if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' send_info when UNKNOWN ctrace('Unknown Op', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' queue_data(UNKNOWN_OP) when CTRL_C # ctrl+c or ctrl+d for telnet friendly ctrace('CTRL-C encountered', strip_op($&)) if NATSD::Server.trace_flag? return close_connection when CTRL_D # ctrl+d for telnet friendly ctrace('CTRL-D encountered', strip_op($&)) if NATSD::Server.trace_flag? return close_connection else # If we are here we do not have a complete line yet that we understand. # If too big, cut the connection off. if @buf.bytesize > NATSD::Server.max_control_line debug_print_controlline_too_big(@buf.bytesize) close_connection end return end @buf = nil if (@buf && @buf.empty?) when AWAITING_MSG_PAYLOAD return unless (@buf.bytesize >= (@msg_size + CR_LF_SIZE)) msg = @buf.slice(0, @msg_size) ctrace('Processing msg', @msg_sub, @msg_reply, msg) if NATSD::Server.trace_flag? queue_data(OK) if @verbose Server.route_to_subscribers(@msg_sub, @msg_reply, msg) @in_msgs += 1 @in_bytes += @msg_size @buf = @buf.slice((@msg_size + CR_LF_SIZE), @buf.bytesize) @msg_sub = @msg_size = @reply = nil @parse_state = AWAITING_CONTROL_LINE @buf = nil if (@buf && @buf.empty?) end end end def send_info queue_data("INFO #{Server.info_string}#{CR_LF}") end # Placeholder def process_info(info) end def auth_ok?(user, pass) Server.auth_ok?(user, pass) end def process_connect_config(config) @verbose = config['verbose'] unless config['verbose'].nil? @pedantic = config['pedantic'] unless config['pedantic'].nil? return queue_data(OK) unless Server.auth_required? EM.cancel_timer(@auth_pending) if auth_ok?(config['user'], config['pass']) queue_data(OK) if @verbose @auth_pending = nil else error_close AUTH_FAILED debug "Authorization failed for #{type.downcase} connection", cid end end def delete_subscriber(sub) ctrace('DELSUB OP', sub.subject, sub.qgroup, sub.sid) if NATSD::Server.trace_flag? Server.unsubscribe(sub, is_route?) @subscriptions.delete(sub.sid) end def error_close(msg) queue_data(msg) flush_data EM.next_tick { close_connection_after_writing } @closing = true end def debug_print_controlline_too_big(line_size) sizes = "#{pretty_size(line_size)} vs #{pretty_size(NATSD::Server.max_control_line)} max" debug "Control line size exceeded (#{sizes}), closing connection.." end def debug_print_msg_too_big(msg_size) sizes = "#{pretty_size(msg_size)} vs #{pretty_size(NATSD::Server.max_payload)} max" debug "Message payload size exceeded (#{sizes}), closing connection" end def inc_connections Server.num_connections += 1 Server.connections[cid] = self end def dec_connections Server.num_connections -= 1 Server.connections.delete(cid) end def process_unbind dec_connections EM.cancel_timer(@ssl_pending) if @ssl_pending @ssl_pending = nil EM.cancel_timer(@auth_pending) if @auth_pending @auth_pending = nil EM.cancel_timer(@ping_timer) if @ping_timer @ping_timer = nil @subscriptions.each_value { |sub| Server.unsubscribe(sub) } @closing = true end def unbind debug "Client connection closed", client_info, cid process_unbind end def ssl_handshake_completed EM.cancel_timer(@ssl_pending) @ssl_pending = nil cert = get_peer_cert debug "#{type} Certificate:", cert ? cert : 'N/A', cid end # FIXME! Cert accepted by default def ssl_verify_peer(cert) true end def ctrace(*args) trace(args, "c: #{cid}") end def strip_op(op='') op.dup.sub(CR_LF, EMPTY) end def is_route? false end def type 'Client' end end end ================================================ FILE: lib/nats/server/connz.rb ================================================ module NATSD #:nodoc: all class Connz def call(env) c_info = Server.dump_connections qs = env['QUERY_STRING'] if (qs =~ /n=(\d+)/) sort_key = :pending_size n = $1.to_i if (qs =~ /s=(\S+)/) case $1.downcase when 'in_msgs'; sort_key = :in_msgs when 'msgs_from'; sort_key = :in_msgs when 'out_msgs'; sort_key = :out_msgs when 'msgs_to'; sort_key = :out_msgs when 'in_bytes'; sort_key = :in_bytes when 'bytes_from'; sort_key = :in_bytes when 'out_bytes'; sort_key = :out_bytes when 'bytes_to'; sort_key = :out_bytes when 'subs'; sort_key = :subscriptions when 'subscriptions'; sort_key = :subscriptions end end conns = c_info[:connections] c_info[:connections] = conns.sort { |a,b| b[sort_key] <=> a[sort_key] } [0, n] end connz_json = JSON.pretty_generate(c_info) + "\n" hdrs = RACK_JSON_HDR.dup hdrs['Content-Length'] = connz_json.bytesize.to_s [200, hdrs, connz_json] end end class Server class << self def dump_connections conns, total = [], 0 ObjectSpace.each_object(NATSD::Connection) do |c| next if c.closing? total += c.info[:pending_size] conns << c.info end { :pending_size => total, :num_connections => conns.size, :connections => conns } end end end end ================================================ FILE: lib/nats/server/const.rb ================================================ module NATSD #:nodoc: VERSION = '0.5.1' APP_NAME = 'nats-server' DEFAULT_PORT = 4222 DEFAULT_HOST = '0.0.0.0' # Parser AWAITING_CONTROL_LINE = 1 AWAITING_MSG_PAYLOAD = 2 # Ops - See protocol.txt for more info INFO = /\AINFO\s*([^\r\n]*)\r\n/i PUB_OP = /\APUB\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i MSG = /\AMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i SUB_OP = /\ASUB\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?([^\s]+)\r\n/i UNSUB_OP = /\AUNSUB\s+([^\s]+)\s*(\s+(\d+))?\r\n/i PING = /\APING\s*\r\n/i PONG = /\APONG\s*\r\n/i INFO_REQ = /\AINFO_REQ\s*\r\n/i CONNECT = /\ACONNECT\s+([^\r\n]+)\r\n/i UNKNOWN = /\A(.*)\r\n/ CTRL_C = /\006/ CTRL_D = /\004/ ERR_RESP = /\A-ERR\s+('.+')?\r\n/i OK_RESP = /\A\+OK\s*\r\n/i #:nodoc: # RESPONSES CR_LF = "\r\n".freeze CR_LF_SIZE = CR_LF.bytesize EMPTY = ''.freeze OK = "+OK#{CR_LF}".freeze PING_RESPONSE = "PING#{CR_LF}".freeze PONG_RESPONSE = "PONG#{CR_LF}".freeze INFO_RESPONSE = "#{CR_LF}".freeze # ERR responses PAYLOAD_TOO_BIG = "-ERR 'Payload size exceeded'#{CR_LF}".freeze PROTOCOL_OP_TOO_BIG = "-ERR 'Protocol Operation size exceeded'#{CR_LF}".freeze INVALID_SUBJECT = "-ERR 'Invalid Subject'#{CR_LF}".freeze INVALID_SID_TAKEN = "-ERR 'Invalid Subject Identifier (sid), already taken'#{CR_LF}".freeze INVALID_SID_NOEXIST = "-ERR 'Invalid Subject-Identifier (sid), no subscriber registered'#{CR_LF}".freeze INVALID_CONFIG = "-ERR 'Invalid config, valid JSON required for connection configuration'#{CR_LF}".freeze AUTH_REQUIRED = "-ERR 'Authorization is required'#{CR_LF}".freeze AUTH_FAILED = "-ERR 'Authorization failed'#{CR_LF}".freeze SSL_REQUIRED = "-ERR 'TSL/SSL is required'#{CR_LF}".freeze SSL_FAILED = "-ERR 'TLS/SSL failed'#{CR_LF}".freeze UNKNOWN_OP = "-ERR 'Unknown Protocol Operation'#{CR_LF}".freeze SLOW_CONSUMER = "-ERR 'Slow consumer detected, connection dropped'#{CR_LF}".freeze UNRESPONSIVE = "-ERR 'Unresponsive client detected, connection dropped'#{CR_LF}".freeze MAX_CONNS_EXCEEDED = "-ERR 'Maximum client connections exceeded, connection dropped'#{CR_LF}".freeze # Pedantic Mode SUB = /^([^\.\*>\s]+|>$|\*)(\.([^\.\*>\s]+|>$|\*))*$/ SUB_NO_WC = /^([^\.\*>\s]+)(\.([^\.\*>\s]+))*$/ # Router Subscription Identifiers RSID = /RSID:(\d+):(\S+)/ # Some sane default thresholds # 1k should be plenty since payloads sans connect string are separate MAX_CONTROL_LINE_SIZE = 1024 # Should be using something different if > 1MB payload MAX_PAYLOAD_SIZE = (1024*1024) # Maximum outbound size per client MAX_PENDING_SIZE = (10*1024*1024) # Maximum pending bucket size MAX_WRITEV_SIZE = (64*1024) # Maximum connections default DEFAULT_MAX_CONNECTIONS = (64*1024) # TLS/SSL wait time SSL_TIMEOUT = 0.5 # Authorization wait time AUTH_TIMEOUT = SSL_TIMEOUT + 0.5 # Ping intervals DEFAULT_PING_INTERVAL = 120 DEFAULT_PING_MAX = 2 # Route Reconnect DEFAULT_ROUTE_RECONNECT_INTERVAL = 1.0 # HTTP RACK_JSON_HDR = { 'Content-Type' => 'application/json' } RACK_TEXT_HDR = { 'Content-Type' => 'text/plain' } end ================================================ FILE: lib/nats/server/options.rb ================================================ require 'optparse' require 'yaml' module NATSD class Server class << self def parser @parser ||= OptionParser.new do |opts| opts.banner = "Usage: nats-server [options]" opts.separator "" opts.separator "Server options:" opts.on("-a", "--addr HOST", "Bind to HOST address " + "(default: #{DEFAULT_HOST})") { |host| @options[:addr] = host } opts.on("-p", "--port PORT", "Use PORT (default: #{DEFAULT_PORT})") { |port| @options[:port] = port.to_i } opts.on("-d", "--daemonize", "Run daemonized in the background") { @options[:daemonize] = true } opts.on("-P", "--pid FILE", "File to store PID") { |file| @options[:pid_file] = file } opts.on("-m", "--http_port PORT", "Use HTTP PORT ") { |port| @options[:http_port] = port.to_i } opts.on("-r", "--cluster_port PORT", "Use Cluster PORT ") { |port| @options[:cluster_port] = port.to_i } opts.on("-c", "--config FILE", "Configuration File") { |file| @options[:config_file] = file } opts.separator "" opts.separator "Logging options:" opts.on("-l", "--log FILE", "File to redirect log output") { |file| @options[:log_file] = file } opts.on("-T", "--logtime", "Timestamp log entries (default: false)") { @options[:log_time] = true } opts.on("-S", "--syslog IDENT", "Enable Syslog output") { |ident| @options[:syslog] = ident } opts.on("-D", "--debug", "Enable debugging output") { @options[:debug] = true } opts.on("-V", "--trace", "Trace the raw protocol") { @options[:trace] = true } opts.separator "" opts.separator "Authorization options:" opts.on("--user user", "User required for connections") { |user| @options[:user] = user } opts.on("--pass password", "Password required for connections") { |pass| @options[:pass] = pass } opts.separator "" opts.on("--ssl", "Enable SSL") { |ssl| @options[:ssl] = true } opts.separator "" opts.separator "Advanced IO options:" opts.on("--no_epoll", "Disable epoll (Linux)") { @options[:noepoll] = true } opts.on("--no_kqueue", "Disable kqueue (MacOSX and BSD)") { @options[:nokqueue] = true } opts.separator "" opts.separator "Common options:" opts.on_tail("-h", "--help", "Show this message") { puts opts; exit } opts.on_tail('-v', '--version', "Show version") { puts NATSD::Server.version; exit } end end def read_config_file return unless config_file = @options[:config_file] config = File.open(config_file) { |f| YAML.load(f) } # Command lines args, parsed first, will override these. @options[:port] = config['port'] if @options[:port].nil? @options[:addr] = config['net'] if @options[:addr].nil? if auth = config['authorization'] @options[:user] = auth['user'] if @options[:user].nil? @options[:pass] = auth['password'] if @options[:pass].nil? @options[:pass] = auth['pass'] if @options[:pass].nil? @options[:token] = auth['token'] if @options[:token].nil? @options[:auth_timeout] = auth['timeout'] if @options[:auth_timeout].nil? # Multiple Users setup @options[:users] = symbolize_users(auth['users']) || [] end # TLS/SSL @options[:ssl] = config['ssl'] if @options[:ssl].nil? @options[:pid_file] = config['pid_file'] if @options[:pid_file].nil? @options[:log_file] = config['log_file'] if @options[:log_file].nil? @options[:log_time] = config['logtime'] if @options[:log_time].nil? @options[:syslog] = config['syslog'] if @options[:syslog].nil? @options[:debug] = config['debug'] if @options[:debug].nil? @options[:trace] = config['trace'] if @options[:trace].nil? # these just override if present @options[:max_control_line] = config['max_control_line'] if config['max_control_line'] @options[:max_payload] = config['max_payload'] if config['max_payload'] @options[:max_pending] = config['max_pending'] if config['max_pending'] @options[:max_connections] = config['max_connections'] if config['max_connections'] # just set @options[:noepoll] = config['no_epoll'] if config['no_epoll'] @options[:nokqueue] = config['no_kqueue'] if config['no_kqueue'] if http = config['http'] if @options[:http_net].nil? @options[:http_net] = http['net'] || @options[:addr] end @options[:http_port] = http['port'] if @options[:http_port].nil? @options[:http_user] = http['user'] if @options[:http_user].nil? @options[:http_password] = http['password'] if @options[:http_password].nil? end if ping = config['ping'] @options[:ping_interval] = ping['interval'] if @options[:ping_interval].nil? @options[:ping_max] = ping['max_outstanding'] if @options[:ping_max].nil? end if cluster = config['cluster'] @options[:cluster_port] = cluster['port'] if @options[:cluster_port].nil? if auth = cluster['authorization'] @options[:cluster_user] = auth['user'] if @options[:cluster_user].nil? @options[:cluster_pass] = auth['password'] if @options[:cluster_pass].nil? @options[:cluster_pass] = auth['pass'] if @options[:cluster_pass].nil? @options[:cluster_token] = auth['token'] if @options[:cluster_token].nil? @options[:cluster_auth_timeout] = auth['timeout'] if @options[:cluster_auth_timeout].nil? @route_auth_required = true end if routes = cluster['routes'] @options[:cluster_routes] = routes if @options[:cluster_routes].nil? end end rescue => e log "Could not read configuration file: #{e}" exit 1 end def setup_logs return unless @options[:log_file] $stdout.reopen(@options[:log_file], 'a') $stdout.sync = true $stderr.reopen($stdout) end def open_syslog return unless @options[:syslog] Syslog.open("#{@options[:syslog]}", Syslog::LOG_PID, Syslog::LOG_USER) unless Syslog.opened? end def close_syslog Syslog.close if @options[:syslog] end def symbolize_users(users) return nil unless users auth_users = [] users.each do |u| auth_users << { :user => u['user'], :pass => u['pass'] || u['password'] } end auth_users end def finalize_options # Addr/Port @options[:port] ||= DEFAULT_PORT @options[:addr] ||= DEFAULT_HOST # Max Connections @options[:max_connections] ||= DEFAULT_MAX_CONNECTIONS @max_connections = @options[:max_connections] # Debug and Tracing @debug_flag = @options[:debug] @trace_flag = @options[:trace] # Log timestamps @log_time = @options[:log_time] debug @options # Block pass? debug "DEBUG is on" trace "TRACE is on" # Syslog @syslog = @options[:syslog] # Authorization # Multi-user setup for auth if @options[:user] # Multiple Users setup @options[:users] ||= [] @options[:users].unshift({:user => @options[:user], :pass => @options[:pass]}) if @options[:user] elsif @options[:users] first = @options[:users].first @options[:user], @options[:pass] = first[:user], first[:pass] end @auth_required = (not @options[:user].nil?) @ssl_required = @options[:ssl] # Pings @options[:ping_interval] ||= DEFAULT_PING_INTERVAL @ping_interval = @options[:ping_interval] @options[:ping_max] ||= DEFAULT_PING_MAX @ping_max = @options[:ping_max] # Thresholds @options[:max_control_line] ||= MAX_CONTROL_LINE_SIZE @max_control_line = @options[:max_control_line] @options[:max_payload] ||= MAX_PAYLOAD_SIZE @max_payload = @options[:max_payload] @options[:max_pending] ||= MAX_PENDING_SIZE @max_pending = @options[:max_pending] @options[:auth_timeout] ||= AUTH_TIMEOUT @auth_timeout = @options[:auth_timeout] @options[:ssl_timeout] ||= SSL_TIMEOUT @ssl_timeout = @options[:ssl_timeout] end end end end ================================================ FILE: lib/nats/server/route.rb ================================================ module NATSD #:nodoc: all # Need to make this a class with EM > 1.0 class Route < EventMachine::Connection #:nodoc: include Connection attr_reader :rid, :remote_rid, :closing, :r_obj, :reconnecting alias :peer_info :client_info alias :reconnecting? :reconnecting def initialize(route=nil) @r_obj = route end def solicited? r_obj != nil end def connection_completed debug "Route connected", rid return unless reconnecting? # Kill reconnect if we got here from there cancel_reconnect @buf, @closing = nil, false post_init end def post_init @rid = Server.rid @subscriptions = {} @in_msgs = @out_msgs = @in_bytes = @out_bytes = 0 @writev_size = 0 @parse_state = AWAITING_CONTROL_LINE # Queue up auth if needed and we solicited the connection debug "Route connection created", peer_info, rid # queue up auth if needed and we solicited the connection if solicited? debug "Route sent authorization", rid send_auth else # FIXME, separate variables for timeout? @auth_pending = EM.add_timer(NATSD::Server.auth_timeout) { connect_auth_timeout } if Server.route_auth_required? end send_info @ping_timer = EM.add_periodic_timer(NATSD::Server.ping_interval) { send_ping } @pings_outstanding = 0 inc_connections send_local_subs_to_route end # TODO: Make sure max_requested is also propogated on reconnect def send_local_subs_to_route ObjectSpace.each_object(NATSD::Connection) do |c| next if c.closing? || c.type != 'Client' c.subscriptions.each_value do |sub| queue_data(NATSD::Server.route_sub_proto(sub)) end end end def process_connect_route_config(config) @verbose = config['verbose'] unless config['verbose'].nil? @pedantic = config['pedantic'] unless config['pedantic'].nil? return queue_data(OK) unless Server.route_auth_required? EM.cancel_timer(@auth_pending) if auth_ok?(config['user'], config['pass']) debug "Route received proper credentials", rid queue_data(OK) if @verbose @auth_pending = nil else error_close AUTH_FAILED debug "Authorization failed for #{type.downcase} connection", rid end end def connect_auth_timeout error_close AUTH_REQUIRED debug "#{type} connection timeout due to lack of auth credentials", rid end def receive_data(data) @buf = @buf ? @buf << data : data return close_connection if @buf =~ /(\006|\004)/ # ctrl+c or ctrl+d for telnet friendly while (@buf && !@closing) case @parse_state when AWAITING_CONTROL_LINE case @buf when MSG ctrace('MSG OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' @parse_state = AWAITING_MSG_PAYLOAD @msg_sub, @msg_sid, @msg_reply, @msg_size = $1, $2, $4, $5.to_i if (@msg_size > NATSD::Server.max_payload) debug_print_msg_too_big(@msg_size) error_close PAYLOAD_TOO_BIG end queue_data(INVALID_SUBJECT) if (@pedantic && !(@msg_sub =~ SUB_NO_WC)) when SUB_OP ctrace('SUB OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' sub, qgroup, sid = $1, $3, $4 return queue_data(INVALID_SUBJECT) if !($1 =~ SUB) return queue_data(INVALID_SID_TAKEN) if @subscriptions[sid] sub = Subscriber.new(self, sub, sid, qgroup, 0) @subscriptions[sid] = sub Server.subscribe(sub, is_route?) queue_data(OK) if @verbose when UNSUB_OP ctrace('UNSUB OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' sid, sub = $1, @subscriptions[$1] if sub # If we have set max_responses, we will unsubscribe once we have received # the appropriate amount of responses. sub.max_responses = ($2 && $3) ? $3.to_i : nil delete_subscriber(sub) unless (sub.max_responses && (sub.num_responses < sub.max_responses)) queue_data(OK) if @verbose else queue_data(INVALID_SID_NOEXIST) if @pedantic end when PING ctrace('PING OP') if NATSD::Server.trace_flag? @buf = $' queue_data(PONG_RESPONSE) flush_data when PONG ctrace('PONG OP') if NATSD::Server.trace_flag? @buf = $' @pings_outstanding -= 1 when CONNECT ctrace('CONNECT OP', strip_op($&)) if NATSD::Server.trace_flag? @buf = $' begin config = JSON.parse($1) process_connect_route_config(config) rescue => e queue_data(INVALID_CONFIG) log_error end when INFO_REQ ctrace('INFO_REQUEST OP') if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' send_info when INFO ctrace('INFO OP', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' process_info($1) when ERR_RESP ctrace('-ERR', $1) if NATSD::Server.trace_flag? close_connection exit when OK_RESP ctrace('+OK') if NATSD::Server.trace_flag? @buf = $' when UNKNOWN ctrace('Unknown Op', strip_op($&)) if NATSD::Server.trace_flag? return connect_auth_timeout if @auth_pending @buf = $' queue_data(UNKNOWN_OP) else # If we are here we do not have a complete line yet that we understand. # If too big, cut the connection off. if @buf.bytesize > NATSD::Server.max_control_line debug_print_controlline_too_big(@buf.bytesize) close_connection end return end @buf = nil if (@buf && @buf.empty?) when AWAITING_MSG_PAYLOAD return unless (@buf.bytesize >= (@msg_size + CR_LF_SIZE)) msg = @buf.slice(0, @msg_size) ctrace('Processing routed msg', @msg_sub, @msg_reply, msg) if NATSD::Server.trace_flag? queue_data(OK) if @verbose # We deliver normal subscriptions like a client publish, which # eliminates the duplicate traversal over the route. However, # qgroups are sent individually per group for only the route # with the intended subscriber, since route interest is L2 # semantics, we deliver those direct. if (sub = Server.rsid_qsub(@msg_sid)) # Allows nil reply to not have extra space reply = @msg_reply + ' ' if @msg_reply Server.deliver_to_subscriber(sub, @msg_sub, reply, msg) else Server.route_to_subscribers(@msg_sub, @msg_reply, msg, is_route?) end @in_msgs += 1 @in_bytes += @msg_size @buf = @buf.slice((@msg_size + CR_LF_SIZE), @buf.bytesize) @msg_sub = @msg_size = @reply = nil @parse_state = AWAITING_CONTROL_LINE @buf = nil if (@buf && @buf.empty?) end end end def send_auth return unless r_obj[:uri].user cs = { :user => r_obj[:uri].user, :pass => r_obj[:uri].password } queue_data("CONNECT #{cs.to_json}#{CR_LF}") end def send_info queue_data("INFO #{Server.route_info_string}#{CR_LF}") end def process_info(info_json) info = JSON.parse(info_json) @remote_rid = info['server_id'] unless info['server_id'].nil? super(info_json) end def auth_ok?(user, pass) Server.route_auth_ok?(user, pass) end def inc_connections Server.num_routes += 1 Server.add_route(self) end def dec_connections Server.num_routes -= 1 Server.remove_route(self) end def try_reconnect debug "Trying to reconnect route", peer_info, rid EM.reconnect(r_obj[:uri].host, r_obj[:uri].port, self) end def cancel_reconnect EM.cancel_timer(@reconnect_timer) if @reconnect_timer @reconnect_timer = nil @reconnecting = false end def unbind return if reconnecting? debug "Route connection closed", peer_info, rid process_unbind if solicited? @reconnecting = true @reconnect_timer = EM.add_periodic_timer(NATSD::DEFAULT_ROUTE_RECONNECT_INTERVAL) { try_reconnect } end end def ctrace(*args) trace(args, "r: #{rid}") end def is_route? true end def type 'Route' end end end ================================================ FILE: lib/nats/server/server.rb ================================================ require 'set' module NATSD #:nodoc: all # Subscriber Subscriber = Struct.new(:conn, :subject, :sid, :qgroup, :num_responses, :max_responses) class Server class << self attr_reader :id, :info, :log_time, :auth_required, :ssl_required, :debug_flag, :trace_flag, :syslog, :options attr_reader :max_payload, :max_pending, :max_control_line, :auth_timeout, :ssl_timeout, :ping_interval, :ping_max attr_accessor :varz, :healthz, :connections, :max_connections, :num_connections, :in_msgs, :out_msgs, :in_bytes, :out_bytes alias auth_required? :auth_required alias ssl_required? :ssl_required alias debug_flag? :debug_flag alias trace_flag? :trace_flag def version; "nats-server version #{NATSD::VERSION}" end def host; @options[:addr] end def port; @options[:port] end def pid_file; @options[:pid_file] end def process_options(argv=[]) @options = {} # Allow command line to override config file, so do them first. parser.parse!(argv) read_config_file if @options[:config_file] finalize_options rescue OptionParser::InvalidOption => e log_error "Error parsing options: #{e}" exit(1) end def setup(argv) process_options(argv) @id, @cid, @rid = fast_uuid, 1, 1 @sublist = Sublist.new @connections = {} @num_connections = 0 @in_msgs = @out_msgs = 0 @in_bytes = @out_bytes = 0 @num_routes = 0 @info = { :server_id => Server.id, :host => host, :port => port, :version => VERSION, :auth_required => auth_required?, :ssl_required => ssl_required?, :max_payload => @max_payload } # Check for daemon flag if @options[:daemonize] require 'rubygems' require 'daemons' require 'tmpdir' unless @options[:log_file] # These log messages visible to controlling TTY log "Starting #{NATSD::APP_NAME} version #{NATSD::VERSION} on port #{NATSD::Server.port}" log "Starting http monitor on port #{@options[:http_port]}" if @options[:http_port] log "Starting routing on port #{@options[:cluster_port]}" if @options[:cluster_port] log "Switching to daemon mode" end opts = { :app_name => APP_NAME, :mode => :exec, :dir_mode => :normal, :dir => Dir.tmpdir } Daemons.daemonize(opts) FileUtils.rm_f("#{Dir.tmpdir}/#{APP_NAME}.pid") end setup_logs open_syslog # Setup optimized select versions EM.epoll unless @options[:noepoll] EM.kqueue unless @options[:nokqueue] # Write pid file if requested. File.open(@options[:pid_file], 'w') { |f| f.puts "#{Process.pid}" } if @options[:pid_file] end def subscribe(sub, is_route=false) @sublist.insert(sub.subject, sub) broadcast_sub_to_routes(sub) unless is_route end def unsubscribe(sub, is_route=false) @sublist.remove(sub.subject, sub) broadcast_unsub_to_routes(sub) unless is_route end def deliver_to_subscriber(sub, subject, reply, msg) conn = sub.conn # Accounting @out_msgs += 1 conn.out_msgs += 1 unless msg.nil? mbs = msg.bytesize @out_bytes += mbs conn.out_bytes += mbs end conn.queue_data("MSG #{subject} #{sub.sid} #{reply}#{msg.bytesize}#{CR_LF}#{msg}#{CR_LF}") # Account for these response and check for auto-unsubscribe (pruning interest graph) sub.num_responses += 1 conn.delete_subscriber(sub) if (sub.max_responses && sub.num_responses >= sub.max_responses) # Check the outbound queue here and react if need be.. if (conn.get_outbound_data_size + conn.writev_size) > NATSD::Server.max_pending conn.error_close SLOW_CONSUMER maxp = pretty_size(NATSD::Server.max_pending) log "Slow consumer dropped, exceeded #{maxp} pending", conn.client_info end end def route_to_subscribers(subject, reply, msg, is_route=false) qsubs = nil # Allows nil reply to not have extra space reply = reply + ' ' if reply # Accounting @in_msgs += 1 @in_bytes += msg.bytesize unless msg.nil? # Routes routes = nil @sublist.match(subject).each do |sub| # Skip anyone in the closing state next if sub.conn.closing # Skip all routes if sourced from another route (1-hop semantics) next if (is_route && sub.conn.is_route?) if sub[:qgroup].nil? if sub.conn.is_route? # Only send messages once over a given route routes ||= Set.new deliver_to_subscriber(sub, subject, reply, msg) unless routes.include?(sub.conn.remote_rid) routes << sub.conn.remote_rid else deliver_to_subscriber(sub, subject, reply, msg) end elsif !is_route if NATSD::Server.trace_flag? trace('Matched queue subscriber', sub[:subject], sub[:qgroup], sub[:sid], sub.conn.client_info) end # Queue this for post processing qsubs ||= Hash.new qsubs[sub[:qgroup]] ||= [] qsubs[sub[:qgroup]] << sub end end return unless qsubs qsubs.each_value do |subs| # Randomly pick a subscriber from the group sub = subs[rand*subs.size] if NATSD::Server.trace_flag? trace('Selected queue subscriber', sub[:subject], sub[:qgroup], sub[:sid], sub.conn.client_info) end deliver_to_subscriber(sub, subject, reply, msg) end end def auth_ok?(user, pass) @options[:users].each { |u| return true if (user == u[:user] && pass == u[:pass]) } false end def cid @cid += 1 end def rid @rid += 1 end def info_string @info.to_json end # Monitoring def start_http_server return unless port = @options[:http_port] require 'thin' log "Starting http monitor on port #{port}" @healthz = "ok\n" @varz = { :start => Time.now, :options => @options, :cores => num_cpu_cores } http_server = Thin::Server.new(@options[:http_net], port, :signals => false) do Thin::Logging.silent = true if NATSD::Server.options[:http_user] auth = [NATSD::Server.options[:http_user], NATSD::Server.options[:http_password]] use Rack::Auth::Basic do |username, password| [username, password] == auth end end map '/healthz' do run lambda { |env| [200, RACK_TEXT_HDR, NATSD::Server.healthz] } end map '/varz' do run Varz.new end map '/connz' do run Connz.new end end http_server.start! end end end end ================================================ FILE: lib/nats/server/sublist.rb ================================================ #-- # # Sublist implementation for a publish-subscribe system. # This container class holds subscriptions and matches # candidate subjects to those subscriptions. # Certain wildcards are supported for subscriptions. # '*' will match any given token at any level. # '>' will match all subsequent tokens. #-- # See included test for example usage: ## class Sublist #:nodoc: PWC = '*'.freeze FWC = '>'.freeze CACHE_SIZE = 4096 attr_reader :count SublistNode = Struct.new(:leaf_nodes, :next_level) SublistLevel = Struct.new(:nodes, :pwc, :fwc) EMPTY_LEVEL = SublistLevel.new({}) def initialize(options = {}) @count = 0 @results = [] @root = SublistLevel.new({}) @cache = {} end # Ruby is a great language to make selective trade offs of space versus time. # We do that here with a low tech front end cache. The cache holds results # until it is exhausted or if the instance inserts or removes a subscription. # The assumption is that the cache is best suited for high speed matching, # and that once it is cleared out it will naturally fill with the high speed # matches. This can obviously be improved with a smarter LRU structure that # does not need to completely go away when a remove happens.. # # front end caching is on by default, but we can turn it off here if needed def disable_cache; @cache = nil; end def enable_cache; @cache ||= {}; end def clear_cache; @cache = {} if @cache; end # Random removal def prune_cache return unless @cache keys = @cache.keys @cache.delete(keys[rand(keys.size)]) end # Insert a subscriber into the sublist for the given subject. def insert(subject, subscriber) # TODO - validate subject as correct. level, tokens = @root, subject.split('.') for token in tokens # This is slightly slower than direct if statements, but looks cleaner. case token when FWC then node = (level.fwc || (level.fwc = SublistNode.new([]))) when PWC then node = (level.pwc || (level.pwc = SublistNode.new([]))) else node = ((level.nodes[token]) || (level.nodes[token] = SublistNode.new([]))) end level = (node.next_level || (node.next_level = SublistLevel.new({}))) end node.leaf_nodes.push(subscriber) @count += 1 clear_cache # Clear the cache node.next_level = nil if node.next_level == EMPTY_LEVEL end # Remove a given subscriber from the sublist for the given subject. def remove(subject, subscriber) return unless subject && subscriber remove_level(@root, subject.split('.'), subscriber) end # Match a subject to all subscribers, return the array of matches. def match(subject) return @cache[subject] if (@cache && @cache[subject]) tokens = subject.split('.') @results.clear matchAll(@root, tokens) # FIXME: This is too low tech, will revisit when needed. if @cache prune_cache if @cache.size > CACHE_SIZE @cache[subject] = Array.new(@results).freeze # Avoid tampering of copy end @results end private def matchAll(level, tokens) node, pwc = nil, nil # Define for scope i, ts = 0, tokens.size while (i < ts) do return if level == nil # Handle a full wildcard here by adding all of the subscribers. @results.concat(level.fwc.leaf_nodes) if level.fwc # Handle an internal partial wildcard by branching recursively lpwc = level.pwc matchAll(lpwc.next_level, tokens[i+1, ts]) if lpwc node, pwc = level.nodes[tokens[i]], lpwc #level = node.next_level if node level = node ? node.next_level : nil i += 1 end @results.concat(pwc.leaf_nodes) if pwc @results.concat(node.leaf_nodes) if node end def prune_level(level, node, token) # Prune here if needed. return unless level && node return unless node.leaf_nodes.empty? && (!node.next_level || node.next_level == EMPTY_LEVEL) if node == level.fwc level.fwc = nil elsif node == level.pwc level.pwc = nil else level.nodes.delete(token) end end def remove_level(level, tokens, subscriber) return unless level token = tokens.shift case token when FWC then node = level.fwc when PWC then node = level.pwc else node = level.nodes[token] end return unless node # This could be expensive if a large number of subscribers exist. if tokens.empty? if (node.leaf_nodes && node.leaf_nodes.delete(subscriber)) @count -= 1 prune_level(level, node, token) clear_cache # Clear the cache end else remove_level(node.next_level, tokens, subscriber) prune_level(level, node, token) end end ################################################ # Used for tests on pruning subscription nodes. ################################################ def node_count_level(level, nc) return 0 unless level nc += 1 if level.fwc nc += node_count_level(level.pwc.next_level, nc+1) if level.pwc level.nodes.each_value do |node| nc += node_count_level(node.next_level, nc) end nc += level.nodes.length end def node_count node_count_level(@root, 0) end end ================================================ FILE: lib/nats/server/util.rb ================================================ require 'pp' def fast_uuid #:nodoc: v = [rand(0x0010000),rand(0x0010000),rand(0x0010000), rand(0x0010000),rand(0x0010000),rand(0x1000000)] "%04x%04x%04x%04x%04x%06x" % v end def syslog(args, priority) #:nodoc: Syslog::log(priority, '%s', PP::pp(args.compact, '', 120)) end def log(*args) #:nodoc: return syslog(args, Syslog::LOG_NOTICE) if NATSD::Server.syslog args.unshift(Time.now) if NATSD::Server.log_time PP::pp(args.compact, $stdout, 120) end def debug(*args) #:nodoc: return unless NATSD::Server.debug_flag? return syslog(args, Syslog::LOG_INFO) if NATSD::Server.syslog log(*args) end def trace(*args) #:nodoc: return unless NATSD::Server.trace_flag? return syslog(args, Syslog::LOG_DEBUG) if NATSD::Server.syslog log(*args) end def log_error(e=$!) #:nodoc: debug e, e.backtrace end def uptime_string(delta) num_seconds = delta.to_i days = num_seconds / (60 * 60 * 24); num_seconds -= days * (60 * 60 * 24); hours = num_seconds / (60 * 60); num_seconds -= hours * (60 * 60); minutes = num_seconds / 60; num_seconds -= minutes * 60; "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s" end def pretty_size(size, prec=1) return 'NA' unless size return "#{size}B" if size < 1024 return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024) return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024) return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0)) end def num_cpu_cores if RUBY_PLATFORM =~ /linux/ return `cat /proc/cpuinfo | grep processor | wc -l`.to_i elsif RUBY_PLATFORM =~ /darwin/ `sysctl -n hw.ncpu`.strip.to_i elsif RUBY_PLATFORM =~ /freebsd|netbsd/ `sysctl hw.ncpu`.strip.to_i else return 1 end end def shutdown #:nodoc: puts log 'Server exiting..' NATSD::Server.close_syslog EM.stop if NATSD::Server.pid_file FileUtils.rm(NATSD::Server.pid_file) if File.exists? NATSD::Server.pid_file end exit end ['TERM','INT'].each { |s| trap(s) { shutdown } } # FIXME - Should probably be smarter when lots of connections def dump_connection_state log "Dumping connection state on SIG_USR2" ObjectSpace.each_object(NATSD::Connection) do |c| log c.info unless c.closing? end log 'Connection Dump Complete' end trap('USR2') { dump_connection_state } ================================================ FILE: lib/nats/server/varz.rb ================================================ module NATSD #:nodoc: all class Varz def call(env) varz_json = JSON.pretty_generate(Server.update_varz) + "\n" hdrs = RACK_JSON_HDR.dup hdrs['Content-Length'] = varz_json.bytesize.to_s [200, hdrs, varz_json] end end class Server class << self def update_varz # Snapshot uptime @varz[:uptime] = uptime_string(Time.now - @varz[:start]) # Grab current cpu and memory usage. rss, pcpu = `ps -o rss=,pcpu= -p #{Process.pid}`.split @varz[:mem] = rss.to_i @varz[:cpu] = pcpu.to_f @varz[:connections] = num_connections @varz[:in_msgs] = in_msgs @varz[:out_msgs] = out_msgs @varz[:in_bytes] = in_bytes @varz[:out_bytes] = out_bytes @varz[:routes] = num_routes @last_varz_update = Time.now.to_f varz end end end end ================================================ FILE: lib/nats/server.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'socket' require 'fileutils' require 'pp' require 'syslog' ep = File.expand_path(File.dirname(__FILE__)) require "#{ep}/ext/em" require "#{ep}/ext/bytesize" require "#{ep}/ext/json" require "#{ep}/server/server" require "#{ep}/server/sublist" require "#{ep}/server/connection" require "#{ep}/server/options" require "#{ep}/server/cluster" require "#{ep}/server/route" require "#{ep}/server/const" require "#{ep}/server/util" require "#{ep}/server/varz" require "#{ep}/server/connz" # Do setup NATSD::Server.setup(ARGV.dup) # Event Loop EM.run do log "WARNING: nats-server is deprecated and no longer supported. It will be removed in a future release. See https://github.com/nats-io/gnatsd" log "Starting #{NATSD::APP_NAME} version #{NATSD::VERSION} on port #{NATSD::Server.port}" log "TLS/SSL Support Enabled" if NATSD::Server.options[:ssl] begin EM.set_descriptor_table_size(32768) # Requires Root privileges EM.start_server(NATSD::Server.host, NATSD::Server.port, NATSD::Connection) rescue => e log "Could not start server on port #{NATSD::Server.port}" log_error exit(1) end # Check to see if we need to fire up the http monitor port and server if NATSD::Server.options[:http_port] begin NATSD::Server.start_http_server rescue => e log "Could not start monitoring server on port #{NATSD::Server.options[:http_port]}" log_error exit(1) end end ################### # CLUSTER SETUP ################### # Check to see if we need to fire up a routing listen port if NATSD::Server.options[:cluster_port] begin log "Starting routing on port #{NATSD::Server.options[:cluster_port]}" EM.start_server(NATSD::Server.host, NATSD::Server.options[:cluster_port], NATSD::Route) rescue => e log "Could not start routing server on port #{NATSD::Server.options[:cluster_port]}" log_error exit(1) end end # If we have active connections, solicit them now.. NATSD::Server.solicit_routes if NATSD::Server.options[:cluster_routes] end ================================================ FILE: lib/nats/version.rb ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # module NATS # NOTE: These are all announced to the server on CONNECT VERSION = "0.11.2".freeze LANG = RUBY_ENGINE PROTOCOL_VERSION = 1 end ================================================ FILE: nats.gemspec ================================================ # Copyright 2010-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # lib = File.expand_path('../lib/', __FILE__) $:.unshift lib unless $:.include?(lib) require 'nats/version' spec = Gem::Specification.new do |s| s.name = 'nats' s.version = NATS::VERSION s.summary = 'NATS is an open-source, high-performance, lightweight cloud messaging system.' s.homepage = 'https://nats.io' s.description = 'NATS is an open-source, high-performance, lightweight cloud messaging system.' s.licenses = ['MIT'] s.authors = ['Derek Collison'] s.email = ['derek.collison@gmail.com'] s.add_dependency('eventmachine', '~> 1.2', '>= 1.2') s.require_paths = ['lib'] s.bindir = 'bin' s.executables = ['nats-pub', 'nats-sub', 'nats-queue', 'nats-request'] s.files = %w[ README.md HISTORY.md nats.gemspec Rakefile bin/nats-server bin/nats-sub bin/nats-pub bin/nats-queue bin/nats-top bin/nats-request lib/nats/client.rb lib/nats/nuid.rb lib/nats/version.rb lib/nats/ext/bytesize.rb lib/nats/ext/em.rb lib/nats/ext/json.rb lib/nats/server.rb lib/nats/server/server.rb lib/nats/server/connection.rb lib/nats/server/cluster.rb lib/nats/server/route.rb lib/nats/server/options.rb lib/nats/server/sublist.rb lib/nats/server/const.rb lib/nats/server/util.rb lib/nats/server/varz.rb lib/nats/server/connz.rb ] end ================================================ FILE: scripts/install_gnatsd.sh ================================================ #!/bin/bash set -e export DEFAULT_NATS_SERVER_VERSION=v2.0.0 export NATS_SERVER_VERSION="${NATS_SERVER_VERSION:=$DEFAULT_NATS_SERVER_VERSION}" # check to see if nats-server folder is empty if [ ! "$(ls -A $HOME/nats-server)" ]; then ( mkdir -p $HOME/nats-server cd $HOME/nats-server wget https://github.com/nats-io/nats-server/releases/download/$NATS_SERVER_VERSION/nats-server-$NATS_SERVER_VERSION-linux-amd64.zip -O nats-server.zip unzip nats-server.zip cp nats-server-$NATS_SERVER_VERSION-linux-amd64/nats-server $HOME/nats-server/nats-server ) else echo 'Using cached directory.'; fi ================================================ FILE: spec/.rspec ================================================ -fd -c ================================================ FILE: spec/client/attack_spec.rb ================================================ require 'spec_helper' describe 'Client - server attacks' do TEST_SERVER = 'nats://127.0.0.1:4222' MSG_SIZE = 10 * 1024 * 1024 BIG_MSG = 'a' * MSG_SIZE BAD_SUBJECT = 'b' * 1024 * 1024 before (:each) do @s = NatsServerControl.new(TEST_SERVER, "/tmp/nats_attack.pid") @s.start_server(true) end after (:each) do @s.kill_server end it "should not let us write large control line buffers" do errors = [] with_em_timeout(3) do NATS.on_error do |e| errors << e end nc = NATS.connect(:servers => [TEST_SERVER], :reconnect => false) nc.flush do nc.publish(BAD_SUBJECT, 'a') end end # Just confirm that it is no longer connected expect(NATS.connected?).to be false expect(errors.count >= 1).to eql(true) end it "should not let us write large messages" do errors = [] with_em_timeout(3) do NATS.on_error do |e| errors << e end nc = NATS.connect(:uri => TEST_SERVER, :reconnect => false) nc.flush do nc.publish('foo', BIG_MSG) end end expect(errors.count > 0).to be true # NOTE: Race here on whether getting NATS::ServerError or NATS::ConnectError # in case we have been disconnected before reading the error sent by server. case errors.count when 1 expect(errors[0]).to be_a NATS::ConnectError when 2 expect(errors[0]).to be_a NATS::ServerError expect(errors[1]).to be_a NATS::ConnectError end end it "should complain if we can't kill our server that we started" do errors = [] @s.kill_server with_em_timeout(5) do NATS.on_error do |e| errors << e end NATS.connect(:uri => TEST_SERVER) end expect(errors.first).to be_a(NATS::ConnectError) end end ================================================ FILE: spec/client/auth_spec.rb ================================================ require 'spec_helper' require 'fileutils' describe 'Client - authorization' do USER = 'derek' PASS = 'mypassword' TEST_AUTH_SERVER = "nats://#{USER}:#{PASS}@127.0.0.1:9222" TEST_AUTH_SERVER_NO_CRED = 'nats://127.0.0.1:9222' TEST_AUTH_SERVER_PID = '/tmp/nats_authorization.pid' TEST_AUTH_AUTO_SERVER_PID = '/tmp/nats_auto_authorization.pid' before (:each) do @s = NatsServerControl.new(TEST_AUTH_SERVER, TEST_AUTH_SERVER_PID) @s.start_server end after (:each) do @s.kill_server FileUtils.rm_f TEST_AUTH_SERVER_PID end it 'should fail to connect to an authorized server without proper credentials' do errors = [] with_em_timeout do |future| NATS.on_error do |e| errors << e end NATS.connect(:uri => TEST_AUTH_SERVER_NO_CRED) end expect(errors.count).to eql(2) expect(errors.first).to be_a NATS::AuthError expect(errors.last).to be_a NATS::ConnectError end it 'should take user and password as separate options' do errors = [] with_em_timeout(1) do NATS.on_error do |e| errors << e end NATS.connect(:uri => TEST_AUTH_SERVER_NO_CRED, :user => USER, :pass => PASS) end expect(errors.count).to eql(0) end it 'should not continue to try to connect on unauthorized access' do auth_error_callbacks = 0 connect_error_callbacks = 0 with_em_timeout do # Default error handler raises, so we trap here. NATS.on_error do |e| # disconnects connect_error_callbacks += 1 if e.instance_of? NATS::ConnectError # authorization auth_error_callbacks +=1 if e.instance_of? NATS::AuthError end NATS.connect(:uri => TEST_AUTH_SERVER_NO_CRED) end expect(auth_error_callbacks).to eql(1) expect(connect_error_callbacks).to eql(1) end it 'should remove server from the pool on unauthorized access' do error_cb = 0 connect_cb = false EM.run do # Default error handler raises, so we trap here. NATS.on_error { error_cb += 1 } connected = false NATS.start(:dont_randomize_servers => true, :servers => [TEST_AUTH_SERVER_NO_CRED, TEST_AUTH_SERVER]) do connect_cb = true EM.stop end end expect(error_cb).to eql(1) expect(connect_cb).to eql(true) expect(NATS.client).to_not be(nil) expect(NATS.client.server_pool.size).to eql(1) NATS.stop # clears err_cb end end ================================================ FILE: spec/client/autounsub_spec.rb ================================================ require 'spec_helper' describe 'Client - max responses and auto-unsubscribe' do before(:each) do @s = NatsServerControl.new("nats://127.0.0.1:4222") @s.start_server(true) end after(:each) do @s.kill_server end it "should only receive N msgs when requested: client support" do WANT = 10 SEND = 20 received = 0 NATS.start(:servers => [@s.uri]) do NATS.subscribe('foo', :max => WANT) { received += 1 } (0...SEND).each { NATS.publish('foo', 'hello') } NATS.publish('done') { NATS.stop } end expect(received).to eql(WANT) end it "should only receive N msgs when auto-unsubscribed" do received = 0 NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.unsubscribe(sid, WANT) (0...SEND).each { NATS.publish('foo', 'hello') } NATS.publish('done') { NATS.stop } end expect(received).to eql(WANT) end it "should not complain when unsubscribing an auto-unsubscribed sid" do received = 0 NATS.start do sid = NATS.subscribe('foo', :max => 1) { received += 1 } (0...SEND).each { NATS.publish('foo', 'hello') } NATS.publish('done') { NATS.unsubscribe(sid) NATS.stop } end expect(received).to eql(1) end it "should allow proper override of auto-unsubscribe max variables to lesser value" do received = 0 NATS.start do sid = NATS.subscribe('foo') { received += 1 NATS.unsubscribe(sid, 1) } NATS.unsubscribe(sid, SEND+1) (0...SEND).each { NATS.publish('foo', 'hello') } NATS.publish('done') { NATS.stop } end expect(received).to eql(1) end it "should allow proper override of auto-unsubscribe max variables to higher value" do received = 0 NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.unsubscribe(sid, 2) NATS.unsubscribe(sid, WANT) (0...SEND).each { NATS.publish('foo', 'hello') } NATS.publish('done') { NATS.stop } end expect(received).to eql(WANT) end it "should only receive N msgs using request mode with multiple helpers" do received = 0 NATS.start do # Create 5 identical helpers (0...5).each { NATS.subscribe('help') { |msg, reply| NATS.publish(reply, 'I can help!') } } NATS.request('help', nil, :max => 1) { received += 1 } EM.add_timer(0.1) { NATS.stop } end expect(received).to eql(1) end it "should not leak subscriptions on request that auto-unsubscribe properly with :max" do received = 0 NATS.start do sid = NATS.subscribe('help') { |msg, reply| NATS.publish(reply, 'I can help!') } (1..100).each do NATS.request('help', 'help request', :max => 1) { received += 1 } end NATS.flush do EM.add_timer(0.1) do NATS.unsubscribe(sid) expect(NATS.client.subscription_count).to eql(0) NATS.stop end end end expect(received).to eql(100) end it "should not complain when unsubscribe called on auto-cleaned up subscription" do NATS.start do sid = NATS.subscribe('help') { |msg, reply| NATS.publish(reply, 'I can help!') } rsid = NATS.request('help', 'help request', :max => 1) {} NATS.flush do EM.add_timer(0.1) do expect(NATS.client.subscription_count).to eql(1) NATS.unsubscribe(sid) expect(NATS.client.subscription_count).to eql(0) NATS.unsubscribe(rsid) NATS.stop end end end end end ================================================ FILE: spec/client/binary_msg_spec.rb ================================================ require 'spec_helper' describe 'Client - test binary message payloads to avoid connection drop' do before(:all) do @s = NatsServerControl.new @s.start_server end after(:all) do @s.kill_server end it "should not disconnect us if we send binary data" do got_error = false NATS.on_error { got_error = true; NATS.stop } NATS.start(:reconnect => false) do expect(NATS.connected?).to eql(true) NATS.publish('dont_disconnect_me', "\006") NATS.flush { NATS.stop } end expect(got_error).to eql(false) end end ================================================ FILE: spec/client/client_cluster_config_spec.rb ================================================ require 'spec_helper' describe 'Client - cluster config' do CLUSTER_USER = 'derek' CLUSTER_PASS = 'mypassword' CLUSTER_AUTH_PORT = 9292 CLUSTER_AUTH_SERVER = "nats://#{CLUSTER_USER}:#{CLUSTER_PASS}@127.0.0.1:#{CLUSTER_AUTH_PORT}" CLUSTER_AUTH_SERVER_PID = '/tmp/nats_cluster_authorization.pid' before(:all) do @s = NatsServerControl.new @as = NatsServerControl.new(CLUSTER_AUTH_SERVER, CLUSTER_AUTH_SERVER_PID) end before(:each) do [@s, @as].each do |s| s.start_server(true) unless NATS.server_running? s.uri end end after(:each) do [@s, @as].each do |s| s.kill_server end end it 'should properly process :uri option for multiple servers' do NATS.start(:uri => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223'], :dont_randomize_servers => true) do options = NATS.options expect(options).to be_a(Hash) expect(options).to have_key(:uri) expect(options[:uri]).to eql(['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223']) NATS.stop end end it 'should allow :uris and :servers as aliases' do NATS.start(:uris => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223'], :dont_randomize_servers => true) do options = NATS.options expect(options).to be_a(Hash) expect(options).to have_key(:uris) expect(options[:uris]).to eql(['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223']) NATS.stop end NATS.start(:servers => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223'], :dont_randomize_servers => true) do options = NATS.options expect(options).to be_a(Hash) expect(options).to have_key(:servers) expect(options[:servers]).to eql(['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223']) NATS.stop end end it 'should allow aliases on instance connections' do c1 = c2 = nil NATS.start do c1 = NATS.connect(:uris => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4222']) c2 = NATS.connect(:servers => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4222']) timeout_nats_on_failure end expect(c1).to_not be(nil) expect(c2).to_not be(nil) end it 'should randomize server pool list by default' do servers = ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223', 'nats://127.0.0.1:4224', 'nats://127.0.0.1:4225', 'nats://127.0.0.1:4226', 'nats://127.0.0.1:4227'] NATS.start do NATS.connect(:uri => servers.dup) do |c| sp_servers = [] c.server_pool.each { |s| sp_servers << s[:uri].to_s } expect(sp_servers).to_not eql(servers) end timeout_nats_on_failure end end it 'should not randomize server pool if options suppress' do servers = ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223', 'nats://127.0.0.1:4224', 'nats://127.0.0.1:4225', 'nats://127.0.0.1:4226', 'nats://127.0.0.1:4227'] NATS.start do NATS.connect(:dont_randomize_servers => true, :uri => servers) do |c| sp_servers = [] c.server_pool.each { |s| sp_servers << s[:uri].to_s } expect(sp_servers).to eql(servers) end timeout_nats_on_failure end end it 'should connect to first entry if available' do NATS.start(:dont_randomize_servers => true, :uri => ['nats://127.0.0.1:4222', 'nats://127.0.0.1:4223']) do expect(NATS.client.connected_server).to eql(URI.parse('nats://127.0.0.1:4222')) NATS.stop end end it 'should fail to connect if no servers available' do errors = [] with_em_timeout do NATS.on_error do |e| errors << e end NATS.start(:uri => ['nats://127.0.0.1:4223']) end expect(errors.first).to be_a(NATS::Error) end it 'should connect to another server if first is not available' do NATS.start(:dont_randomize_servers => true, :uri => ['nats://127.0.0.1:4224', 'nats://127.0.0.1:4222']) do expect(NATS.client.connected_server).to eql(URI.parse('nats://127.0.0.1:4222')) NATS.stop end end it 'should fail if all servers are not available' do errors = [] with_em_timeout do NATS.on_error do |e| errors << e end NATS.connect(:uri => ['nats://127.0.0.1:4224', 'nats://127.0.0.1:4223']) end expect(errors.count > 0).to be(true) expect(errors.first).to be_a(NATS::ConnectError) end it 'should fail if server is available but does not have proper auth' do errors = [] with_em_timeout(5) do NATS.on_error do |e| errors << e end NATS.connect(:uri => ['nats://127.0.0.1:4224', "nats://127.0.0.1:#{CLUSTER_AUTH_PORT}"], :dont_randomize_servers => true) end expect(errors.count).to eql(2) expect(errors.first).to be_a(NATS::AuthError) expect(errors.last).to be_a(NATS::ConnectError) end it 'should succeed if proper credentials supplied with non-first uri' do with_em_timeout(3) do # FIXME: Flush should not be required to be able to assert connected URI nc = NATS.connect(:dont_randomize_servers => true, :uri => ['nats://127.0.0.1:4224', CLUSTER_AUTH_SERVER]) nc.flush do expect(nc.connected_server).to eql(URI.parse(CLUSTER_AUTH_SERVER)) end end end it 'should allow user/pass overrides' do s_uri = "nats://127.0.0.1:#{CLUSTER_AUTH_PORT}" errors = [] with_em_timeout(5) do NATS.on_error do |e| errors << e end NATS.connect(:servers => [s_uri]) end expect(errors.count).to eql(2) expect(errors.first).to be_a(NATS::AuthError) expect(errors.last).to be_a(NATS::ConnectError) errors = [] with_em_timeout do NATS.connect(:uri => [s_uri], :user => CLUSTER_USER, :pass => CLUSTER_PASS) do |nc2| nc2.publish("hello", "world") end end expect(errors.count).to eql(0) end context do before(:all) do @s1_uri = 'nats://derek:foo@127.0.0.1:9290' @s1 = NatsServerControl.new(@s1_uri, '/tmp/nats_cluster_honor_s1.pid') @s1.start_server @s2_uri = 'nats://sarah:bar@127.0.0.1:9298' @s2 = NatsServerControl.new(@s2_uri, '/tmp/nats_cluster_honor_s2.pid') @s2.start_server end after(:all) do @s1.kill_server @s2.kill_server end it 'should honor auth credentials properly for listed servers' do with_em_timeout(5) do # FIXME: Flush should not be required, rather connect should be synchronous... nc = NATS.connect(:dont_randomize_servers => true, :servers => [@s1_uri, @s2_uri]) nc.flush do expect(nc.connected_server).to eql(URI.parse(@s1_uri)) # Disconnect from first server kill_time = Time.now @s1.kill_server EM.add_timer(1) do time_diff = Time.now - kill_time expect(time_diff < 2).to eql(true) # Confirm that it has connected to the second server expect(nc.connected_server).to eql(URI.parse(@s2_uri)) # Restart the first server again... @s1.start_server # Kill the second server once again to reconnect to first one... kill_time2 = Time.now @s2.kill_server EM.add_timer(0.25) do time_diff = Time.now - kill_time2 expect(time_diff < 1).to eql(true) # Confirm we are reconnecting to the first one again nc.flush do expect(nc.connected_server).to eql(URI.parse(@s1_uri)) end end end end end end end end ================================================ FILE: spec/client/client_cluster_reconnect_spec.rb ================================================ require 'spec_helper' describe 'Client - cluster reconnect' do before(:all) do auth_options = { 'user' => 'derek', 'password' => 'bella', 'token' => 'deadbeef', 'timeout' => 5 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } s3_config_opts = { 'pid_file' => '/tmp/nats_cluster_s3.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4244, 'cluster_port' => 6224 } nodes = [] configs = [s1_config_opts, s2_config_opts, s3_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { user: '#{auth_options["user"]}' password: '#{auth_options["password"]}' timeout: #{auth_options["timeout"]} } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: 5 } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2, @s3 = nodes end before(:each) do [@s1, @s2, @s3].each do |s| s.start_server(true) end end after(:each) do [@s1, @s2, @s3].each do |s| s.kill_server end end it 'should properly handle exceptions thrown by eventmachine during reconnects' do reconnect_cb = false opts = { :dont_randomize_servers => true, :reconnect_time_wait => 0.25, :servers => [@s1.uri, URI.parse("nats://does.not.exist:4222/"), @s3.uri] } with_em_timeout(5) do nc = NATS.connect(opts) nc.on_reconnect do reconnect_cb = true expect(nc.connected?).to be(true) expect(nc.connected_server).to eql(@s3.uri) end EM.add_timer(1) do @s1.kill_server end end expect(reconnect_cb).to eql(true) end it 'should call reconnect callback when current connection fails' do reconnect_cb = false opts = { :dont_randomize_servers => true, :reconnect_time_wait => 0.25, :servers => [@s1.uri, @s2.uri, @s3.uri], :max_reconnect_attempts => 5 } reconnect_conns = [] with_em_timeout(5) do NATS.start(opts) do |c| expect(c.connected_server).to eql(@s1.uri) c.on_reconnect do |conn| reconnect_cb = true reconnect_conns << conn.connected_server end @s1.kill_server EM.add_timer(1) do @s2.kill_server end end end expect(reconnect_cb).to eq(true) expect(reconnect_conns.count).to eql(2) expect(reconnect_conns.first).to eql(@s2.uri) expect(reconnect_conns.last).to eql(@s3.uri) end it 'should connect to another server if possible before reconnect' do NATS.start(:dont_randomize_servers => true, :servers => [@s1.uri, @s2.uri, @s3.uri]) do |c| timeout_nats_on_failure(15) expect(c.connected_server).to eql(@s1.uri) c.on_reconnect do expect(c.connected_server).to eql(@s2.uri) NATS.stop end @s1.kill_server end end it 'should connect to a previous server if multiple servers exit' do NATS.start(:dont_randomize_servers => true, :servers => [@s1.uri, @s2.uri, @s3.uri]) do |c| timeout_nats_on_failure(15) expect(c.connected_server).to eql(@s1.uri) kill_time = Time.now expected_uri = nil c.on_reconnect do expect(c.connected_server).to eql(expected_uri) if expected_uri == @s2.uri # Expect to connect back to S1 expected_uri = @s1.uri @s1.start_server @s2.kill_server end NATS.stop if c.connected_server == @s1.uri end # Expect to connect to S2 after killing S1 and S3. expected_uri = @s2.uri @s1.kill_server @s3.kill_server end end it 'should use reconnect logic to connect to a previous server if multiple servers exit' do @s2.kill_server # Take this one offline @s3.kill_server # Take this one offline too, since it will be discovered in cluster options = { :dont_randomize_servers => true, :servers => [@s1.uri, @s2.uri], :reconnect => true, :max_reconnect_attempts => 2, :reconnect_time_wait => 1 } with_em_timeout do NATS.start(options) do |c| expect(c.connected_server).to eql(@s1.uri) c.on_reconnect do expect(c.connected?).to eql(true) expect(c.connected_server).to eql(@s1.uri) NATS.stop end @s1.kill_server @s1.start_server end end end context 'when max_reconnect_attempts == -1 (do not remove servers)' do it 'should never remove servers that fail' do options = { :dont_randomize_servers => true, :servers => [@s1.uri, @s2.uri], :reconnect => true, :max_reconnect_attempts => -1, :reconnect_time_wait => 1 } done = false @s1.kill_server with_em_timeout(2) do NATS.start(options) do |c| expect(c.connected_server).to eql(@s2.uri) expect(c.server_pool.size).to eq(3) EM.add_timer(0.5) do @s3.kill_server # Server goes away from the cluster EM.add_timer(1) do done = true expect(c.server_pool.size).to eq(3) end end end end expect(done).to eql(true) end it 'should never remove servers due to client triggered disconnect' do options = { :dont_randomize_servers => true, :servers => [@s1.uri, @s2.uri], :reconnect => true, :max_reconnect_attempts => -1, :reconnect_time_wait => 2 } done = false with_em_timeout(5) do |future| errors = [] reconnects = 0 NATS.start(options) do |nc| nc.on_error do |e| errors << e end nc.on_reconnect do reconnects += 1 end expect(nc.connected_server).to eql(@s1.uri) expect(nc.server_pool.size).to eq(3) messages = [] nc.subscribe("hello") do |msg| messages << msg end payload = nc.server_info[:max_payload] + 100 nc.publish("hello", 'A' * payload) EM.add_timer(1) { @s1.kill_server } EM.add_timer(2.5) do expect(nc.connected_server).to eql(@s2.uri) expect(nc.server_pool.size).to eq(3) expect(messages.count).to eql(0) EM.add_timer(0.5) do @s3.kill_server expect(errors.count).to eql(1) expect(nc.server_pool.size).to eq(3) done = true nc.close future.resume end end end end expect(done).to eql(true) end end end ================================================ FILE: spec/client/client_config_spec.rb ================================================ require 'spec_helper' describe "Client - configuration" do before(:each) do @s = NatsServerControl.new @s.start_server(true) end after(:each) do @s.kill_server end it 'should honor setting options' do with_em_timeout do NATS.start(:debug => true, :pedantic => false, :verbose => true, :reconnect => true, :max_reconnect_attempts => 100, :reconnect_time_wait => 5, :uri => 'nats://127.0.0.1:4222') do options = NATS.options expect(options).to be_a Hash expect(options).to have_key :debug expect(options[:debug]).to eql(true) expect(options).to have_key :pedantic expect(options[:pedantic]).to eql(false) expect(options).to have_key :verbose expect(options[:verbose]).to eql(true) expect(options).to have_key :reconnect expect(options[:reconnect]).to eql(true) expect(options).to have_key :max_reconnect_attempts expect(options[:max_reconnect_attempts]).to eql(100) expect(options).to have_key :reconnect_time_wait expect(options[:reconnect_time_wait]).to eql(5) expect(options).to have_key :uri expect(options[:uri].to_s).to eql('nats://127.0.0.1:4222') NATS.stop end end end it 'should not complain against bad subjects if pedantic mode disabled' do with_em_timeout do |future| expect do nc = NATS.connect(:pedantic => false, :reconnect => false) nc.flush do nc.publish('foo.>.foo', 'hello') do future.resume(nc) end end end.to_not raise_error end end it 'should complain against bad subjects in pedantic mode' do errors = [] with_em_timeout do |future| nc = nil NATS.on_error do |e| errors << e future.resume(nc) end nc = NATS.connect(:pedantic => true, :reconnect => false) nc.flush do nc.publish('foo.>.foo', 'hello') end end expect(errors.count).to eql(1) expect(errors.first).to be_a NATS::ServerError end it 'should set verbose mode correctly' do NATS.start(:debug => true, :pedantic => false, :verbose => true) do NATS.publish('foo') { NATS.stop } end end it 'should have default ping options' do NATS.start do options = NATS.options expect(options).to be_a Hash expect(options).to have_key :ping_interval expect(options[:ping_interval]).to eql(NATS::DEFAULT_PING_INTERVAL) expect(options).to have_key :max_outstanding_pings expect(options[:max_outstanding_pings]).to eql(NATS::DEFAULT_PING_MAX) NATS.stop end end it 'should allow overrides of ping variables' do NATS.start(:ping_interval => 30, :max_outstanding_pings => 4) do options = NATS.options expect(options).to be_a Hash expect(options).to have_key :ping_interval expect(options[:ping_interval]).to eql(30) expect(options).to have_key :max_outstanding_pings expect(options[:max_outstanding_pings]).to eql(4) NATS.stop end end it 'should honor environment vars options' do ENV['NATS_VERBOSE'] = 'true' ENV['NATS_PEDANTIC'] = 'true' ENV['NATS_DEBUG'] = 'true' ENV['NATS_RECONNECT'] = 'true' ENV['NATS_FAST_PRODUCER'] = 'true' ENV['NATS_MAX_RECONNECT_ATTEMPTS'] = '100' ENV['NATS_RECONNECT_TIME_WAIT'] = '5' ENV['NATS_URI'] = 'nats://127.0.0.1:4222' NATS.start do options = NATS.options expect(options).to be_a Hash expect(options).to have_key :debug expect(options[:debug]).to eql(true) expect(options).to have_key :pedantic expect(options[:pedantic]).to eql(true) expect(options).to have_key :verbose expect(options[:verbose]).to eql(true) expect(options).to have_key :reconnect expect(options[:reconnect]).to eql(true) expect(options).to have_key :fast_producer_error expect(options[:fast_producer_error]).to eql(true) expect(options).to have_key :max_reconnect_attempts expect(options[:max_reconnect_attempts]).to eql(100) expect(options).to have_key :reconnect_time_wait expect(options[:reconnect_time_wait]).to eql(5) expect(options).to have_key :uri expect(options[:uri].to_s).to eql('nats://127.0.0.1:4222') NATS.stop end # Restore environment to be without options! ENV.delete 'NATS_VERBOSE' ENV.delete 'NATS_PEDANTIC' ENV.delete 'NATS_DEBUG' ENV.delete 'NATS_RECONNECT' ENV.delete 'NATS_FAST_PRODUCER' ENV.delete 'NATS_MAX_RECONNECT_ATTEMPTS' ENV.delete 'NATS_RECONNECT_TIME_WAIT' ENV.delete 'NATS_URI' end it 'should respect the reconnect parameters' do with_em_timeout do expect do NATS.start(:max_reconnect_attempts => 1, :reconnect_time_wait => 1) do NATS.stop end end.to_not raise_error end # Stop the server, make sure it can't connect and see that the time to fail make sense start_at = nil closed_at = nil errors = [] with_em_timeout(5) do NATS.on_error do |e| errors << e end NATS.on_close do closed_at = Time.now end NATS.start(:max_reconnect_attempts => 1, :reconnect_time_wait => 1) do start_at = Time.now @s.kill_server end end time_diff = closed_at - start_at expect(errors.count).to eql(1) expect(errors.first).to be_a NATS::ConnectError # Check if the reconnect took more than the expected 4 secs... expect(time_diff > 1).to eql(true) expect(time_diff < 4).to eql(true) end end ================================================ FILE: spec/client/client_connect_spec.rb ================================================ require 'spec_helper' describe 'Client - connect' do before(:each) do @s = NatsServerControl.new @s.start_server(true) end after(:each) do @s.kill_server end context "No echo support" do it "should not receive messages when no echo is enabled" do errors = [] msgs_a = [] msgs_b = [] with_em_timeout do NATS.on_error do |e| errors << e end # Client A will receive all messages from B NATS.connect(uri: @s.uri, no_echo: true) do |nc| nc.subscribe("hello") do |msg| msgs_a << msg end EM.add_timer(0.5) do 10.times do nc.publish("hello", "world") end end end # Default is classic behavior of client which is # for echo to be enabled, so will receive messages # from Client A and itself. NATS.connect(uri: @s.uri) do |nc| nc.subscribe("hello") do |msg| msgs_b << msg end EM.add_timer(0.5) do 10.times do nc.publish("hello", "world") end end end end expect(errors.count).to eql(0) expect(msgs_a.count).to eql(10) expect(msgs_b.count).to eql(20) end it "should have no echo enabled by default" do errors = [] msgs_a = [] msgs_b = [] with_em_timeout do NATS.on_error do |e| errors << e end # Client A will receive all messages from B NATS.connect(uri: @s.uri) do |nc| nc.subscribe("hello") do |msg| msgs_a << msg end EM.add_timer(0.5) do 10.times do nc.publish("hello", "world") end end end # Default is classic behavior of client which is # for echo to be enabled, so will receive messages # from Client A and itself. NATS.connect(uri: @s.uri) do |nc| nc.subscribe("hello") do |msg| msgs_b << msg end EM.add_timer(0.5) do 10.times do nc.publish("hello", "world") end end end end expect(errors.count).to eql(0) expect(msgs_a.count).to eql(20) expect(msgs_b.count).to eql(20) end it "should fail if echo is enabled but not supported by server" do expect do with_em_timeout do OldInfoServer.start { NATS.connect(uri: "nats://127.0.0.1:9997", no_echo: true) do |nc| end } end end.to raise_error(NATS::ServerError) end it "should fail if echo is enabled but not supported by server protocol" do expect do with_em_timeout do OldProtocolInfoServer.start { NATS.connect(uri: "nats://127.0.0.1:9996", no_echo: true) do |nc| end } end end.to raise_error(NATS::ServerError) end end context "Simple Connect" do it "should connect using host:port" do with_em_timeout do |future| NATS.connect("127.0.0.1:4222") do |nc| nc.subscribe("foo") do future.resume end nc.publish("foo", "bar") end end end it "should connect with just host using default port" do with_em_timeout do |future| NATS.connect("127.0.0.1") do |nc| nc.subscribe("foo") do future.resume end nc.publish("foo", "bar") end end end it "should connect with just host: using default port" do with_em_timeout do |future| NATS.connect("127.0.0.1:") do |nc| nc.subscribe("foo") do future.resume end nc.publish("foo", "bar") end end end it "should fail to connect with empty string" do expect do with_em_timeout do |future| NATS.connect("") end end.to raise_error(URI::InvalidURIError) end it "should support comma separated list of servers" do options = {} servers = [] with_em_timeout do |future| NATS.connect("nats://127.0.0.1:4222,nats://127.0.0.1:4223,nats://127.0.0.1:4224", dont_randomize_servers: true) do |nc| nc.subscribe("foo") do options = nc.options servers = nc.server_pool future.resume end nc.publish("foo", "bar") end end expect(options[:dont_randomize_servers]).to eql(true) expect(servers.count).to eql(3) servers.each do |server| expect(server[:uri].scheme).to eql('nats') expect(server[:uri].host).to eql('127.0.0.1') end a, b, c = servers expect(a[:uri].port).to eql(4223) expect(b[:uri].port).to eql(4224) expect(c[:uri].port).to eql(4222) end it "should support comma separated list of servers with own user info" do servers = [] with_em_timeout do |future| NATS.connect("nats://a:b@127.0.0.1:4222,nats://c:d@127.0.0.1:4223,nats://e:f@127.0.0.1:4224", dont_randomize_servers: true) do |nc| nc.subscribe("foo") do servers = nc.server_pool future.resume end nc.publish("foo", "bar") end end expect(servers.count).to eql(3) servers.each do |server| expect(server[:uri].scheme).to eql('nats') expect(server[:uri].host).to eql('127.0.0.1') end a, b, c = servers expect(c[:uri].port).to eql(4222) expect(a[:uri].port).to eql(4223) expect(b[:uri].port).to eql(4224) expect(c[:uri].user).to eql('a') expect(a[:uri].user).to eql('c') expect(b[:uri].user).to eql('e') expect(c[:uri].password).to eql('b') expect(a[:uri].password).to eql('d') expect(b[:uri].password).to eql('f') end end end ================================================ FILE: spec/client/client_drain_spec.rb ================================================ require 'spec_helper' describe 'Client - drain' do before(:each) do @s = NatsServerControl.new @s.start_server(true) end after(:each) do @s.kill_server end it "should support draining a connection" do msgs = [] errors = [] closed = false drained = false after_drain = nil total_msgs_before_drain = nil total_msgs_after_drain = nil total_msgs_sent = nil pending_data_before_draining = nil pending_data_after_draining = nil pending_outbound_data_before_draining = nil pending_outbound_data_after_draining = nil with_em_timeout(10) do |future| nc1 = NATS.connect(uri: @s.uri) do |nc| expect(nc.options[:drain_timeout]).to eql(30) nc.on_error do |err| errors << err end nc.on_close do |err| closed = true future.resume end nc.subscribe("foo", queue: "worker") do |msg, reply| nc.publish(reply, "ACK:foo") end nc.subscribe("bar") do |msg, reply| nc.publish(reply, "ACK:bar") end nc.subscribe("quux") do |msg, reply| nc.publish(reply, "ACK:quux") end EM.add_timer(1) do # Before draining pending_data_before_draining = nc.instance_variable_get('@buf') pending_outbound_data_before_draining = nc.pending_data_size total_msgs_before_drain = nc.msgs_received nc.drain do after_drain = nc.draining? drained = true total_msgs_sent = nc.msgs_sent total_msgs_after_drain = nc.msgs_received pending_data_after_draining = nc.instance_variable_get('@buf') pending_outbound_data_after_draining = nc.pending_data_size end end end # Fast publisher nc2 = NATS.connect(uri: @s.uri) do |nc| inbox = NATS.create_inbox nc.subscribe(inbox) do |msg| msgs << msg end timer = EM.add_periodic_timer(0.1) do 10000.times do nc.publish("foo", "hi", inbox) nc.publish("bar", "hi", inbox) nc.publish("quux", "hi", inbox) end end EM.add_timer(1) do EM.cancel_timer(timer) end end end # Should be the same as the messages received by the first client. expect(msgs.count).to eql(total_msgs_sent) expect(msgs.count).to eql(total_msgs_after_drain) expect(pending_outbound_data_after_draining).to eql(0) expect(pending_data_after_draining).to eql(nil) expect(errors.count).to eql(0) expect(closed).to eql(true) expect(drained).to eql(true) expect(after_drain).to eql(false) expect(total_msgs_before_drain < total_msgs_after_drain).to eql(true) expect(pending_data_after_draining).to eql(nil) end it "should timeout draining if takes too long" do msgs = [] errors = [] closed = false drained = false after_drain = nil total_msgs_before_drain = nil total_msgs_after_drain = nil total_msgs_sent = nil pending_data_before_draining = nil pending_data_after_draining = nil pending_outbound_data_before_draining = nil pending_outbound_data_after_draining = nil with_em_timeout(10) do |future| # Use a very short timeout for to timeout. nc1 = NATS.connect(uri: @s.uri, drain_timeout: 0.01) do |nc| nc.on_error do |err| errors << err end nc.on_close do |err| closed = true future.resume end nc.subscribe("foo", queue: "worker") do |msg, reply| nc.publish(reply, "ACK:foo") end nc.subscribe("bar") do |msg, reply| nc.publish(reply, "ACK:bar") end nc.subscribe("quux") do |msg, reply| nc.publish(reply, "ACK:quux") end EM.add_timer(1) do subs = nc.instance_variable_get('@subs') pending_data_before_draining = nc.instance_variable_get('@buf') total_msgs_before_drain = nc.msgs_received nc.drain do after_drain = nc.draining? drained = true pending_data_after_draining = nc.instance_variable_get('@buf') total_msgs_after_drain = nc.msgs_received total_msgs_sent = nc.msgs_sent end end end # Fast publisher nc2 = NATS.connect(uri: @s.uri) do |nc| inbox = NATS.create_inbox nc.subscribe(inbox) do |msg| msgs << msg end timer = EM.add_periodic_timer(0.1) do 10000.times do nc.publish("foo", "hi", inbox) nc.publish("bar", "hi", inbox) nc.publish("quux", "hi", inbox) end end EM.add_timer(1) do EM.cancel_timer(timer) end end end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::ClientError) expect(errors.first.to_s).to eql("Drain Timeout") expect(closed).to eql(true) expect(drained).to eql(true) expect(after_drain).to eql(false) expect(total_msgs_before_drain < total_msgs_after_drain).to eql(true) expect(pending_data_after_draining).to eql(nil) end it "should disallow subscribe and unsubscribe while draining" do msgs = [] errors = [] closed = false drained = false after_drain = nil total_msgs_before_drain = nil total_msgs_after_drain = nil total_msgs_sent = nil pending_data_before_draining = nil pending_data_after_draining = nil pending_outbound_data_before_draining = nil pending_outbound_data_after_draining = nil unsub_result = true no_more_subs = nil with_em_timeout(10) do |future| nc1 = NATS.connect(uri: @s.uri) do |nc| expect(nc.options[:drain_timeout]).to eql(30) nc.on_error do |err| errors << err end nc.on_close do |err| closed = true future.resume end nc.subscribe("foo", queue: "worker") do |msg, reply| nc.publish(reply, "ACK:foo") end nc.subscribe("bar") do |msg, reply| nc.publish(reply, "ACK:bar") end nc.subscribe("quux") do |msg, reply| nc.publish(reply, "ACK:quux") end sub_timer = EM.add_periodic_timer(0.1) do sid = nc.subscribe("hello.#{rand(1_000_000)}") { } if sid.nil? no_more_subs = true EM.cancel_timer(sub_timer) # Any sid even if invalid should return right away unsub_result = nc.unsubscribe(1) end end EM.add_timer(1) do pending_data_before_draining = nc.instance_variable_get('@buf') pending_outbound_data_before_draining = nc.pending_data_size total_msgs_before_drain = nc.msgs_received nc.drain do after_drain = nc.draining? drained = true total_msgs_sent = nc.msgs_sent total_msgs_after_drain = nc.msgs_received pending_data_after_draining = nc.instance_variable_get('@buf') pending_outbound_data_after_draining = nc.pending_data_size end end end # Fast publisher nc2 = NATS.connect(uri: @s.uri) do |nc| inbox = NATS.create_inbox nc.subscribe(inbox) do |msg| msgs << msg end timer = EM.add_periodic_timer(0.1) do 10000.times do nc.publish("foo", "hi", inbox) nc.publish("bar", "hi", inbox) nc.publish("quux", "hi", inbox) end end EM.add_timer(1) do EM.cancel_timer(timer) end end end # Subscribe should have eventually failed expect(no_more_subs).to eql(true) expect(unsub_result).to eql(nil) # Should be the same as the messages received by the first client. expect(msgs.count).to eql(total_msgs_sent) expect(msgs.count).to eql(total_msgs_after_drain) expect(pending_outbound_data_after_draining).to eql(0) expect(pending_data_after_draining).to eql(nil) expect(errors.count).to eql(0) expect(closed).to eql(true) expect(drained).to eql(true) expect(after_drain).to eql(false) expect(total_msgs_before_drain < total_msgs_after_drain).to eql(true) expect(pending_data_after_draining).to eql(nil) end it "should disallow publish and flush outbound pending data once subscriptions have been drained" do msgs = [] errors = [] closed = false drained = false after_drain = nil total_msgs_received_before_drain = nil total_msgs_received_after_drain = nil total_msgs_sent = nil pending_data_before_draining = nil pending_data_after_draining = nil pending_outbound_data_before_draining = nil pending_outbound_data_after_draining = nil before_publish = nil after_publish = nil extra_pubs = 0 with_em_timeout(30) do |future| nc1 = NATS.connect(uri: @s.uri) do |nc| expect(nc.options[:drain_timeout]).to eql(30) nc.on_error do |err| errors << err end nc.on_close do |err| closed = true # Give sometime to the other client to receive # all the messages that were published by client # that started to drain. EM.add_timer(5) do future.resume end end nc.subscribe("foo", queue: "worker") do |msg, reply| 10.times { nc.publish(reply, "ACK:foo") } end nc.subscribe("bar") do |msg, reply| 10.times { nc.publish(reply, "ACK:bar") } end nc.subscribe("quux") do |msg, reply| 10.times { nc.publish(reply, "ACK:quux") } end EM.add_timer(0.5) do pub_timer = EM.add_periodic_timer(0.1) do before_publish = nc.msgs_sent nc.publish("hello", "world") after_publish = nc.msgs_sent if before_publish == after_publish EM.cancel_timer(pub_timer) else extra_pubs += 1 end end end EM.add_timer(1.5) do pending_data_before_draining = nc.instance_variable_get('@buf') pending_outbound_data_before_draining = nc.pending_data_size total_msgs_received_before_drain = nc.msgs_received nc.drain do after_drain = nc.draining? drained = true total_msgs_sent = nc.msgs_sent total_msgs_received_after_drain = nc.msgs_received pending_data_after_draining = nc.instance_variable_get('@buf') pending_outbound_data_after_draining = nc.pending_data_size end end end # Fast publisher nc2 = NATS.connect(uri: @s.uri) do |nc| inbox = NATS.create_inbox nc.flush do nc.subscribe(inbox) do |msg| msgs << msg end end timer = EM.add_periodic_timer(0.2) do 10000.times do nc.publish("foo", "hi", inbox) nc.publish("bar", "hi", inbox) nc.publish("quux", "hi", inbox) end end EM.add_timer(1) do EM.cancel_timer(timer) end end end # Should be the same as the messages received by the first client. expect(msgs.count).to eql(total_msgs_sent-extra_pubs) expect(msgs.count).to eql(total_msgs_received_after_drain*10) expect(before_publish).to eql(after_publish) expect(pending_outbound_data_after_draining).to eql(0) if not pending_outbound_data_after_draining.nil? expect(pending_data_after_draining).to eql(nil) expect(errors.count).to eql(0) expect(closed).to eql(true) expect(drained).to eql(true) expect(after_drain).to eql(false) expect(pending_data_after_draining).to eql(nil) end it "should support draining a connection with NATS.drain" do msgs = [] drained = false after_drain = nil total_msgs_before_drain = nil total_msgs_after_drain = nil total_msgs_sent = nil pending_data_before_draining = nil pending_data_after_draining = nil pending_outbound_data_before_draining = nil pending_outbound_data_after_draining = nil with_em_timeout(10) do NATS.start(uri: @s.uri) do |nc| expect(nc.options[:drain_timeout]).to eql(30) NATS.subscribe("foo", queue: "worker") do |msg, reply| NATS.publish(reply, "ACK:foo") end NATS.subscribe("bar") do |msg, reply| NATS.publish(reply, "ACK:bar") end NATS.subscribe("quux") do |msg, reply| NATS.publish(reply, "ACK:quux") end EM.add_timer(1) do # Before draining pending_data_before_draining = nc.instance_variable_get('@buf') pending_outbound_data_before_draining = nc.pending_data_size total_msgs_before_drain = nc.msgs_received NATS.drain do after_drain = nc.draining? drained = true total_msgs_sent = nc.msgs_sent total_msgs_after_drain = nc.msgs_received pending_data_after_draining = nc.instance_variable_get('@buf') pending_outbound_data_after_draining = nc.pending_data_size end end end # Fast publisher NATS.connect(uri: @s.uri) do |nc| inbox = NATS.create_inbox nc.subscribe(inbox) do |msg| msgs << msg end timer = EM.add_periodic_timer(0.1) do 10000.times do nc.publish("foo", "hi", inbox) nc.publish("bar", "hi", inbox) nc.publish("quux", "hi", inbox) end end EM.add_timer(1) do EM.cancel_timer(timer) end end end # Should be the same as the messages received by the first client. expect(msgs.count).to eql(total_msgs_sent) expect(msgs.count).to eql(total_msgs_after_drain) expect(pending_outbound_data_after_draining).to eql(0) expect(pending_data_after_draining).to eql(nil) expect(drained).to eql(true) expect(after_drain).to eql(false) expect(total_msgs_before_drain < total_msgs_after_drain).to eql(true) expect(pending_data_after_draining).to eql(nil) end end ================================================ FILE: spec/client/client_nkeys_connect_spec.rb ================================================ require 'spec_helper' describe 'Client - NATS v2 Auth' do context 'with NKEYS and JWT' do before(:each) do config_opts = { 'pid_file' => '/tmp/nats_nkeys_jwt.pid', 'host' => '127.0.0.1', 'port' => 4722, } @s = NatsServerControl.init_with_config_from_string(%Q( authorization { timeout: 2 } port = #{config_opts['port']} operator = "./spec/configs/nkeys/op.jwt" # This is for account resolution. resolver = MEMORY # This is a map that can preload keys:jwts into a memory resolver. resolver_preload = { # foo AD7SEANS6BCBF6FHIB7SQ3UGJVPW53BXOALP75YXJBBXQL7EAFB6NJNA : "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiIyUDNHU1BFSk9DNlVZNE5aM05DNzVQVFJIV1pVRFhPV1pLR0NLUDVPNjJYSlZESVEzQ0ZRIiwiaWF0IjoxNTUzODQwNjE1LCJpc3MiOiJPRFdJSUU3SjdOT1M3M1dWQk5WWTdIQ1dYVTRXWFdEQlNDVjRWSUtNNVk0TFhUT1Q1U1FQT0xXTCIsIm5hbWUiOiJmb28iLCJzdWIiOiJBRDdTRUFOUzZCQ0JGNkZISUI3U1EzVUdKVlBXNTNCWE9BTFA3NVlYSkJCWFFMN0VBRkI2TkpOQSIsInR5cGUiOiJhY2NvdW50IiwibmF0cyI6eyJsaW1pdHMiOnsic3VicyI6LTEsImNvbm4iOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwid2lsZGNhcmRzIjp0cnVlfX19.COiKg5EFK4Gb2gA7vtKHQK7vjMEUx-RMWYuN-Bg-uVOFs9GLwW7Dxc4TcN-poBGBEkwKnleiA9SjYO3y4-AqBQ" # bar AAXPTP32BD73YW3ACUY6DPXKWBSUW4VEZNE3LD4FUOFDP6KDU43PQVU2 : "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJPQ1dUQkRQTzVETjRSV0lFNEtJQ1BQWkszUEhHV0dQUVFKNFVET1pQSTVaRzJQUzZKVkpBIiwiaWF0IjoxNTUzODQwNjE5LCJpc3MiOiJPRFdJSUU3SjdOT1M3M1dWQk5WWTdIQ1dYVTRXWFdEQlNDVjRWSUtNNVk0TFhUT1Q1U1FQT0xXTCIsIm5hbWUiOiJiYXIiLCJzdWIiOiJBQVhQVFAzMkJENzNZVzNBQ1VZNkRQWEtXQlNVVzRWRVpORTNMRDRGVU9GRFA2S0RVNDNQUVZVMiIsInR5cGUiOiJhY2NvdW50IiwibmF0cyI6eyJsaW1pdHMiOnsic3VicyI6LTEsImNvbm4iOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwid2lsZGNhcmRzIjp0cnVlfX19.KY2fBvYyNCA0dYS7I6_rETGHT4YGkWZSh03XhXxwAvJ8XCfKlVJRY82U-0ERg01SFtPTZ-6BYu-sty1E67ioDA" } ), config_opts) @s.start_server(true) end after(:each) do @s.kill_server end it 'should connect to server and publish messages' do with_em_timeout do |f| NATS.start(servers: ["nats://127.0.0.1:4722"], user_credentials: "./spec/configs/nkeys/foo-user.creds") do NATS.subscribe("hello") do |msg| f.resume end NATS.publish('hello', 'world') end end end it 'should fail with auth error if no user credentials present' do expect do NATS.start(servers: ["nats://127.0.0.1:4722"]) do NATS.publish('hello', 'world') do EM.stop end end end.to raise_error(NATS::AuthError) end end context 'with NKEYS only' do before(:each) do config_opts = { 'pid_file' => '/tmp/nats_nkeys.pid', 'host' => '127.0.0.1', 'port' => 4723, } @s = NatsServerControl.init_with_config_from_string(%Q( authorization { timeout: 2 } port = #{config_opts['port']} accounts { acme { users [ { nkey = "UCK5N7N66OBOINFXAYC2ACJQYFSOD4VYNU6APEJTAVFZB2SVHLKGEW7L", permissions = { subscribe = { allow = ["hello", "_INBOX.>"] deny = ["foo"] } publish = { allow = ["hello", "_INBOX.>"] deny = ["foo"] } } } ] } } ), config_opts) @s.start_server(true) end after(:each) do @s.kill_server end it 'should connect to the server and publish messages' do with_em_timeout do |f| NATS.start(servers: ["nats://127.0.0.1:4723"], nkeys_seed: "./spec/configs/nkeys/foo-user.nk") do NATS.subscribe("hello") do |msg| f.resume end NATS.publish('hello', 'world') end end end end end ================================================ FILE: spec/client/client_requests_spec.rb ================================================ require 'spec_helper' describe 'Client - requests' do before(:each) do @s = NatsServerControl.new @s.start_server(true) end after(:each) do @s.kill_server end it 'should receive responses using single subscription for requests' do msgs = [] received = false nats = nil NATS.start do |nc| nats = nc nc.subscribe('need_help') do |msg, reply| nc.publish(reply, "help-#{msg}") end Fiber.new do msgs << nc.request('need_help', 'yyy') msgs << nc.request('need_help', 'zzz') received = true NATS.stop end.resume timeout_nats_on_failure end expect(received).to eql(true) expect(msgs.first).to eql('help-yyy') expect(msgs.last).to eql('help-zzz') resp_map = nats.instance_variable_get('@resp_map') expect(resp_map.keys.count).to eql(0) end it 'should receive a response from a request' do received = false nats = nil NATS.start do |nc| nats = nc nc.subscribe('need_help') do |msg, reply| expect(msg).to eql('yyy') nc.publish(reply, 'help') end Fiber.new do response = nc.request('need_help', 'yyy') received = true expect(response).to eql('help') NATS.stop end.resume timeout_nats_on_failure end expect(received).to eql(true) resp_map = nats.instance_variable_get('@resp_map') expect(resp_map.keys.count).to eql(0) end it 'should perform similar using class mirror functions' do received = false NATS.start do s = NATS.subscribe('need_help') do |msg, reply| expect(msg).to eql('yyy') NATS.publish(reply, 'help') NATS.unsubscribe(s) end Fiber.new do response = NATS.request('need_help', 'yyy') received = true expect(response).to eql('help') NATS.stop end.resume timeout_nats_on_failure end expect(received).to eql(true) end it 'should be possible to gather multiple responses before a timeout' do nats = nil responses = [] NATS.start do |nc| nats = nc 10.times do |n| nc.subscribe('need_help') do |msg, reply| expect(msg).to eql('yyy') nc.publish(reply, "help-#{n}") end end Fiber.new do responses = nc.request('need_help', 'yyy', max: 5, timeout: 1) NATS.stop end.resume timeout_nats_on_failure(2) end expect(responses.count).to eql(5) # NOTE: Behavior change here in NATS v1.2.0, now responses # are not in the same order as the subscriptions. # responses.each_with_index do |response, i| # expect(response).to eql("help-#{i}") # end expect(responses.count).to eql(5) resp_map = nats.instance_variable_get('@resp_map') expect(resp_map.keys.count).to eql(0) end it 'should be possible to gather single response from a queue group before a timeout' do nats = nil responses = [] NATS.start do |nc| nats = nc 10.times do |n| nc.subscribe('need_help', queue: 'worker') do |msg, reply| expect(msg).to eql('yyy') nc.publish(reply, "help") end end Fiber.new do responses = nc.request('need_help', 'yyy', max: 5, timeout: 1) NATS.stop end.resume timeout_nats_on_failure(2) end expect(responses.count).to eql(1) responses.each_with_index do |response, i| expect(response).to eql("help") end resp_map = nats.instance_variable_get('@resp_map') expect(resp_map.keys.count).to eql(0) end it 'should be possible to gather as many responses as possible before the timeout' do nats = nil responses = [] NATS.start do |nc| nats = nc nc.subscribe('need_help') do |msg, reply| 3.times do |n| nc.publish(reply, "help-#{n}") end EM.add_timer(1) do 3.upto(10).each do |n| nc.publish(reply, "help-#{n}") end end end Fiber.new do responses = nc.request('need_help', 'yyy', max: 5, timeout: 0.5) NATS.stop end.resume timeout_nats_on_failure(2) end # Expected 5 but only 3 made it. expect(responses.count).to eql(3) responses.each_with_index do |response, i| expect(response).to eql("help-#{i}") end resp_map = nats.instance_variable_get('@resp_map') expect(resp_map.keys.count).to eql(0) end it 'should return empty array in case waited many responses but got none before timeout' do nats = nil responses = [] NATS.start do |nc| nats = nc Fiber.new do responses = nc.request('need_help', 'yyy', max: 5, timeout: 0.5) NATS.stop end.resume timeout_nats_on_failure(2) end expect(responses.count).to eql(0) resp_map = nats.instance_variable_get('@resp_map') expect(resp_map.keys.count).to eql(0) end it 'should return nil in case waited single response but got none before timeout' do response = nil NATS.start do |nc| Fiber.new do response = nc.request('need_help', 'yyy') NATS.stop end.resume timeout_nats_on_failure(2) end expect(response).to eql(nil) end it 'should fail if not wrapped in a fiber' do NATS.start do expect do NATS.request('need_help', 'yyy') end.to raise_error(FiberError) NATS.stop end end end ================================================ FILE: spec/client/client_spec.rb ================================================ require 'spec_helper' describe 'Client - specification' do before(:each) do @s = NatsServerControl.new @s.start_server(true) end after(:each) do @s.kill_server end it "should complain if it can't connect to server when not running" do errors = [] with_em_timeout do NATS.on_error do |e| errors << e end NATS.connect(:uri => 'nats://127.0.0.1:3222') end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::ConnectError) end it 'should complain if NATS.start is called without EM running and no block was given', :jruby_excluded do expect(EM.reactor_running?).to eql(false) expect { NATS.start }.to raise_error(NATS::Error) expect(NATS.connected?).to eql(false) end it 'should report supplied connection name' do @s.kill_server @s.start_server(true, monitoring: true) NATS.start(uri: 'nats://127.0.0.1:4222', name: 'test-connection') do expect(JSON.parse(Net::HTTP.get(URI('http://localhost:8222/connz')))['connections'][0]['name']).to eq 'test-connection' NATS.stop end end it 'should perform basic block start and stop' do NATS.start { NATS.stop } end it 'should signal connected state' do NATS.start do expect(NATS.connected?).to eql(true) NATS.stop end end it 'should have err_cb cleared after stop' do NATS.start do NATS.on_error { puts 'err' } NATS.stop end expect(NATS.err_cb).to eql(nil) end it 'should raise NATS::ServerError on error replies from NATS Server' do skip 'trying to unsubscribe non existant subscription does not send -ERR back in pedantic mode on gnatsd' expect do NATS.start(:pedantic => true) do NATS.unsubscribe(10000) NATS.publish('done') { NATS.stop } end end.to raise_error(NATS::ServerError) end it 'should do publish without payload and with opt_reply without error' do NATS.start { |nc| nc.publish('foo') nc.publish('foo', 'hello') nc.publish('foo', 'hello', 'reply') NATS.stop } end it 'should not complain when publishing to nil' do errors = [] with_em_timeout do NATS.on_error do |e| errors << e end NATS.start do NATS.publish(nil) NATS.publish(nil, 'hello') end end expect(errors.count).to eql(0) end it 'should receive a sid when doing a subscribe' do sid = nil errors = [] with_em_timeout do NATS.on_error do |e| errors << e end NATS.connect do |nc| sid = nc.subscribe('foo') end end expect(errors.count).to eql(0) expect(sid).to_not eql(nil) end it 'should receive a sid when doing a request' do sid = nil errors = [] with_em_timeout do NATS.on_error do |e| errors << e end NATS.start do |nc| sid = nc.request('foo') { } end end expect(sid).to_not eql(nil) end it 'should receive a message that it has a subscription to' do received = false received_msg = nil with_em_timeout do NATS.start do |nc| nc.subscribe('foo') do |msg| received = true received_msg = msg NATS.stop end nc.publish('foo', 'xxx') end end expect(received).to eql(true) expect(received_msg).to eql('xxx') end it 'should receive a message that it has a wildcard subscription to' do received = false with_em_timeout do NATS.start do |nc| nc.subscribe('*') do |msg| received = true expect(msg).to eql('xxx') NATS.stop end nc.publish('foo', 'xxx') end end expect(received).to eql(true) end it 'should not receive a message that it has unsubscribed from' do sid = nil received = 0 msgs = [] with_em_timeout do NATS.start do |nc| sid = nc.subscribe('*') do |msg| received += 1 msgs << msg nc.unsubscribe(sid) end nc.publish('foo', 'xxx') end end expect(received).to eql(1) expect(msgs.first).to eql('xxx') end it 'should receive a response from a request' do received = false NATS.start do |nc| nc.subscribe('need_help') do |msg, reply| expect(msg).to eql('yyy') nc.publish(reply, 'help') end nc.request('need_help', 'yyy') do |response| received = true expect(response).to eql('help') NATS.stop end timeout_nats_on_failure end expect(received).to eql(true) end it 'should perform similar using class mirror functions' do received = false NATS.start do s = NATS.subscribe('need_help') do |msg, reply| expect(msg).to eql('yyy') NATS.publish(reply, 'help') NATS.unsubscribe(s) end r = NATS.request('need_help', 'yyy') do |response| received = true expect(response).to eql('help') NATS.unsubscribe(r) NATS.stop end timeout_nats_on_failure end expect(received).to eql(true) end it 'should return inside closure on publish when server received msg' do received_pub_closure = false NATS.start { NATS.publish('foo') { received_pub_closure = true NATS.stop } timeout_nats_on_failure } expect(received_pub_closure).to eql(true) end it 'should return inside closure in ordered fashion when server received msg' do replies = [] expected = [] received_pub_closure = false NATS.start { (1..100).each { |i| expected << i NATS.publish('foo') { replies << i } } NATS.publish('foo') { received_pub_closure = true NATS.stop } timeout_nats_on_failure } expect(received_pub_closure).to eql(true) expect(replies).to eql(expected) end it "should be able to start and use a new connection inside of start block" do new_conn = nil received = false NATS.start { NATS.subscribe('foo') { received = true; NATS.stop } new_conn = NATS.connect do new_conn.publish('foo', 'hello') end timeout_nats_on_failure(5) } expect(new_conn).to_not eql(nil) expect(received).to eql(true) end it 'should allow proper request/reply across multiple connections' do new_conn = nil received_request = false received_reply = false NATS.start { new_conn = NATS.connect new_conn.subscribe('test_conn_rr') do |msg, reply| received_request = true new_conn.publish(reply) end new_conn.on_connect do NATS.request('test_conn_rr') do received_reply = true NATS.stop end end timeout_nats_on_failure } expect(new_conn).to_not eql(nil) expect(received_request).to eql(true) expect(received_reply).to eql(true) end it 'should complain if NATS.start called without a block when we would need to start EM' do expect do NATS.start NATS.stop end.to raise_error(NATS::Error) end it 'should not complain if NATS.start called without a block when EM is running already', :jruby_excluded do skip 'flapping in newer Rubies' EM.run do expect do NATS.start EM.next_tick { NATS.stop { EM.stop } } end.to_not raise_error end end it 'should use default url if passed uri is nil' do NATS.start(:uri => nil) { NATS.stop } end it 'should not complain about publish to nil unless in pedantic mode' do NATS.start { NATS.publish(nil, 'Hello!') NATS.stop } end it 'should allow proper unsubscribe from within blocks' do received = 0 NATS.start do sid = NATS.subscribe('foo') { |msg| received += 1 expect(sid).to_not eql(true) NATS.unsubscribe(sid) } NATS.publish('foo') NATS.publish('foo') { NATS.stop } end expect(received).to eql(1) end it 'should not call error handler for double unsubscribe unless in pedantic mode' do got_error = false NATS.on_error { got_error = true; NATS.stop } NATS.start do s = NATS.subscribe('foo') NATS.unsubscribe(s) NATS.unsubscribe(s) NATS.publish('flush') { NATS.stop } end expect(got_error).to eql(false) end it 'should call error handler for double unsubscribe if in pedantic mode' do skip 'double unsubscribe in gnatsd does not send -ERR back' got_error = false NATS.on_error { got_error = true; NATS.stop } NATS.start(:pedantic => true) do s = NATS.subscribe('foo') NATS.unsubscribe(s) NATS.unsubscribe(s) NATS.publish('flush') { NATS.stop } end expect(got_error).to eql(true) end it 'should monitor inbound and outbound messages and bytes' do msg = 'Hello World!' NATS.start do |c| NATS.subscribe('foo') NATS.publish('foo', msg) NATS.publish('bar', msg) NATS.flush do expect(c.msgs_sent).to eql(2) expect(c.msgs_received).to eql(1) expect(c.bytes_received).to eql(msg.size) expect(c.bytes_sent).to eql(msg.size * 2) NATS.stop end end end it "should allow getting snapshot of inbound and outbound stats" do stats = nil with_em_timeout do NATS.start do |nats| # Snapshot the stats from either of the callbacks nats.subscribe(">") { stats = nats.stats } nats.subscribe("foo") { stats = nats.stats } nats.flush do nats.publish("foo", "world") end end end expect(stats[:in_msgs]).to eql(2) expect(stats[:in_bytes]).to eql(10) expect(stats[:out_msgs]).to eql(1) expect(stats[:out_bytes]).to eql(5) end it 'should receive a pong from a server after ping_interval' do NATS.start(:ping_interval => 0.75) do expect(NATS.client.pongs_received).to eql(0) EM.add_timer(1) do expect(NATS.client.pongs_received).to eql(1) NATS.stop end end end it 'should disconnect from the server when pongs not received' do EM.run do NATS.connect(:ping_interval => 0.1, :max_outstanding_pings => 1, :reconnect => false) do |c| c.on_error { NATS.stop } def c.process_pong # override to not process counters end end EM.add_timer(0.5) do expect(NATS.connected?).to eql(false) EM.stop end end end it 'should stop the ping timer when disconnected or closed' do EM.run do $pings_received = 0 NATS.connect(:ping_interval => 0.1) do |c| def c.send_ping $pings_received += 1 close end end EM.add_timer(0.5) do expect($pings_received).to eql(1) EM.stop end end end it 'should allowing setting name for the client on connect' do with_em_timeout do connect_command = "CONNECT {\"verbose\":false,\"pedantic\":false,\"lang\":\"#{NATS::LANG}\",\"version\":\"#{NATS::VERSION}\",\"protocol\":#{NATS::PROTOCOL_VERSION},\"echo\":true,\"name\":\"hello\"}\r\n" conn = NATS.connect(:name => "hello") expect(conn.connect_command).to eql(connect_command) end end it 'should not repeat SUB commands when connecting' do pending_commands = "CONNECT {\"verbose\":false,\"pedantic\":true,\"lang\":\"#{NATS::LANG}\",\"version\":\"#{NATS::VERSION}\",\"protocol\":#{NATS::PROTOCOL_VERSION},\"echo\":true}\r\n" pending_commands += "PING\r\n" pending_commands += "SUB hello 2\r\nSUB hello 3\r\nSUB hello 4\r\nSUB hello 5\r\nSUB hello 6\r\n" msgs = [] expect do EM.run do EM.add_timer(1) do expect(msgs.count).to eql(5) EM.stop end conn = NATS.connect(:pedantic => true) expect(conn).to receive(:send_data).once.with(pending_commands).and_call_original 5.times do conn.subscribe("hello") do |msg| msgs << msg end end # Expect INFO followed by PONG response expect(conn).to receive(:receive_data).at_least(:twice).and_call_original expect(conn).to receive(:send_data).once.with("PUB hello 5\r\nworld\r\n").and_call_original conn.flush do # Once we connected already and received PONG back, # we should be able to publish here. conn.publish("hello", "world") end end end.to_not raise_error end it 'should accept the same option set twice' do opts = {:uri => 'nats://127.0.0.1:4222'} NATS.start(opts) { NATS.stop } NATS.start(opts) { NATS.stop } end describe '#create_inbox' do it 'create the expected format' do expect(NATS.create_inbox).to match(/_INBOX\.[a-f0-9]{12}/) end context 'when Kernel.srand is regularly reset to the same value' do it 'should generate a unique inbox name' do Kernel.srand 5555 first_inbox_name = NATS.create_inbox Kernel.srand 5555 second_inbox_name = NATS.create_inbox expect(second_inbox_name).to_not eq(first_inbox_name) end end end end ================================================ FILE: spec/client/client_tls_spec.rb ================================================ require 'spec_helper' describe 'Client - TLS spec', :jruby_excluded do context 'when server does not support TLS' do before(:each) do @non_tls_server = NatsServerControl.new("nats://127.0.0.1:4222") @non_tls_server.start_server end after(:each) do @non_tls_server.kill_server end it 'should error if client requires TLS' do errors = [] closes = 0 reconnects = 0 disconnects = 0 options = { :uri => 'nats://127.0.0.1:4222', :reconnect => false, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :verify_peer => false } } with_em_timeout(5) do |future| nc = nil NATS.on_error {|e| errors << e } NATS.on_close { closes += 1 } NATS.on_reconnect { reconnects += 1 } NATS.on_disconnect { disconnects += 1 } nc = NATS.connect(options) end expect(errors.count).to eql(2) expect(errors.first).to be_a(NATS::ClientError) expect(errors.first.to_s).to eql("TLS/SSL not supported by server") expect(errors.last).to be_a(NATS::ConnectError) expect(closes).to eql(1) expect(reconnects).to eql(0) # Technically we were never connected to the NATS service # in that server so we don't call disconnect right now. expect(disconnects).to eql(0) end end context 'when server requires TLS and no auth needed' do before(:each) do @tls_no_auth = NatsServerControl.new("nats://127.0.0.1:4444", '/tmp/test-nats-4444.pid', "-c ./spec/configs/tls-no-auth.conf") @tls_no_auth.start_server end after(:each) do @tls_no_auth.kill_server end it 'should error if client does not set secure connection and dispatch callbacks' do errors = [] closes = 0 reconnects = 0 disconnects = 0 reconnects = 0 with_em_timeout(3) do |future| nc = nil NATS.on_close { closes += 1 } NATS.on_reconnect { reconnects += 1 } NATS.on_disconnect { disconnects += 1 } NATS.on_error do |e| errors << e end nc = NATS.connect(:uri => 'nats://127.0.0.1:4444', :reconnect => false) end expect(errors.count > 0).to eq(true) expect(errors.first).to be_a(NATS::ClientError) expect(closes).to eql(1) expect(reconnects).to eql(0) expect(disconnects).to eql(1) end end context 'when server requires TLS and authentication' do before(:each) do @tls_auth = NatsServerControl.new("nats://127.0.0.1:4443", '/tmp/test-nats-4443.pid', "-c ./spec/configs/tls.conf") @tls_auth.start_server end after(:each) do @tls_auth.kill_server end it 'should error if client does not set secure connection and dispatch callbacks' do errors = [] with_em_timeout(3) do |future| nc = nil NATS.on_error do|e| errors << e end nc = NATS.connect(:uri => 'nats://127.0.0.1:4443', :reconnect => false) end # Client disconnected from server expect(errors.count > 0).to eq(true) expect(errors.first).to be_a(NATS::ClientError) end it 'should error if client does not set secure connection and stop trying to reconnect eventually' do errors = [] reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| nc = nil NATS.on_error do |e| errors << e end NATS.on_disconnect do disconnects += 1 end NATS.on_reconnect do |conn| expect(conn.connected_server).to eql(URI.parse('nats://127.0.0.1:4443')) reconnects += 1 end NATS.on_close do # NOTE: We cannot close again here in tests since # we would be getting double fiber called errors. # future.resume(nc) if not nc.closing? closes += 1 future.resume end nc = NATS.connect({ :servers => ['nats://127.0.0.1:4443'], :max_reconnect_attempts => 2, :reconnect_time_wait => 1 }) end # FIXME: It will be trying to reconnect for a number of times # and some of the erros that will be getting could be errors # such as Unknown Protocol due to parser failing with secure conn. expect(reconnects).to eql(3) expect(disconnects).to eql(4) expect(closes).to eql(1) expect(errors.count > 0).to eq(true) expect(errors.count < 10).to eq(true) # Client disconnected from server expect(errors.first).to be_a(NATS::ClientError) expect(errors.last).to be_a(NATS::ConnectError) end it 'should reject secure connection when using deprecated versions' do errors = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| nc = nil NATS.on_error do |e| errors << e end NATS.on_disconnect do disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end nc = NATS.connect({ :servers => ['nats://secret:deadbeef@127.0.0.1:4443'], :tls => { :ssl_version => :sslv2 }}) do connects += 1 end nc.subscribe("hello") nc.flush do nc.close end end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::ConnectError) expect(connects).to eql(0) expect(closes).to eql(1) expect(disconnects).to eql(0) expect(reconnects).to eql(0) end it 'should connect securely to server and authorize with default TLS and protocols options' do errors = [] messages = [] connects = 0 disconnects = 0 reconnects = 0 closes = 0 with_em_timeout(10) do NATS.on_error do |e| errors << e end NATS.on_disconnect do disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :max_reconnect_attempts => 1, :dont_randomize_servers => true, } nc = NATS.connect("tls://secret:deadbeef@127.0.0.1:4443", options) do |nc2| server = nc2.connected_server expect(server.scheme).to eql("tls") expect(server.host).to eql("127.0.0.1") expect(server.port).to eql(4443) expect(server.userinfo).to eql("secret:deadbeef") connects += 1 info = nc2.server_info expect(info).to_not be(nil) expect(info).to be_a Hash expect(info).to have_key :server_id expect(info).to have_key :version expect(info).to have_key :proto expect(info).to have_key :client_id expect(info).to have_key :max_payload expect(info).to have_key :host expect(info).to have_key :port expect(info).to have_key :auth_required expect(info).to have_key :tls_required end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close expect(messages.count).to eql(1) end end end expect(errors.count).to eql(0) # We are calling close so should not be calling # the disconnect callback here. expect(disconnects).to eql(0) expect(connects).to eql(1) expect(closes).to eql(1) expect(reconnects).to eql(0) end it 'should connect securely to server and authorize with defaults only via setting ssl enabled option' do errors = [] messages = [] connects = 0 disconnects = 0 reconnects = 0 closes = 0 with_em_timeout(10) do NATS.on_error do |e| errors << e end NATS.on_disconnect do disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4443'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :ssl => true } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4443')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close end end end expect(messages.count).to eql(1) expect(errors.count).to eql(0) # We are calling close so should not be calling # the disconnect callback here. expect(disconnects).to eql(0) expect(closes).to eql(1) expect(reconnects).to eql(0) end it 'should connect securely with default TLS and protocols options' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_close do closes += 1 end NATS.on_reconnect do reconnects += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4443'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { # :ssl_version => :TLSv1_2, # :protocols => [:tlsv1_2], # :private_key_file => './spec/configs/certs/key.pem', # :cert_chain_file => './spec/configs/certs/server.pem', # :verify_peer => true } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4443')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close end end end expect(errors.count).to eql(0) expect(messages.count).to eql(1) expect(reconnects).to eql(0) expect(closes).to eql(1) expect(disconnects).to eql(0) end end context 'when server requires TLS, certificates and authentication' do before(:each) do @tls_verify_auth = NatsServerControl.new("nats://127.0.0.1:4445", '/tmp/test-nats-4445.pid', "-c ./spec/configs/tlsverify.conf") @tls_verify_auth.start_server end after(:each) do @tls_verify_auth.kill_server end it 'should error if client does not set secure connection and dispatch callbacks' do errors = [] with_em_timeout(10) do |future| nc = nil NATS.on_error do |e| errors << e end nc = NATS.connect(:uri => 'nats://127.0.0.1:4445', :reconnect => false) end expect(errors.count >= 2).to eql(true) # Client disconnected from server expect(errors.first).to be_a(NATS::ClientError) expect(errors.last).to be_a(NATS::ConnectError) end it 'should error if client does not set secure connection and stop trying to reconnect eventually' do errors = [] reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| nc = nil NATS.on_error do |e| errors << e end NATS.on_disconnect do disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do # NOTE: We cannot close again here in tests since # we would be getting double fiber called errors. # future.resume(nc) if not nc.closing? closes += 1 end nc = NATS.connect(:uri => 'nats://127.0.0.1:4445', :reconnect_time_wait => 1, :max_reconnect_attempts => 2) end expect(errors.count > 2).to eql(true) expect(errors.count < 10).to eql(true) expect(disconnects).to eql(4) expect(reconnects).to eql(3) expect(closes).to eql(1) # Client disconnected from server expect(errors.first).to be_a(NATS::ClientError) expect(errors.last).to be_a(NATS::ConnectError) end it 'should reject secure connection if no certificate is provided' do errors = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(5) do |future| nc = nil NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end nc = NATS.connect({ :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :tls => { :ssl_version => :TLSv1_2 } }) do connects += 1 end nc.subscribe("hello") nc.flush end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::ConnectError) expect(connects).to eql(0) expect(closes).to eql(1) expect(disconnects).to eql(0) end it 'should connect securely to server and authorize' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :verify_peer => false } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close end end end expect(errors.count).to eql(0) expect(messages.count).to eql(1) expect(disconnects).to eql(0) expect(closes).to eql(1) end it 'should connect securely with default TLS and protocols options' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close end end end expect(messages.count).to eql(1) expect(errors.count).to eql(0) expect(closes).to eql(1) expect(reconnects).to eql(0) expect(disconnects).to eql(0) end end context 'when server requires TLS, certificates, authentication and client enables verify peer' do before(:each) do @tls_verify_auth = NatsServerControl.new("nats://127.0.0.1:4445", '/tmp/test-nats-4445.pid', "-c ./spec/configs/tlsverify.conf") @tls_verify_auth.start_server end after(:each) do @tls_verify_auth.kill_server end it 'should fail to connect if CA is not given' do errors = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(3) do nc = nil NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :verify_peer => true } } expect do nc = NATS.connect(options) do |conn| connects += 1 end end.to raise_error(NATS::Error) end expect(errors.count).to eql(0) expect(disconnects).to eql(0) expect(closes).to eql(0) end it 'should fail to connect if CA is not readable' do errors = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(3) do nc = nil NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :ca_file => './spec/configs/certs/does-not-exists.pem', :verify_peer => true } } expect do nc = NATS.connect(options) do |conn| connects += 1 end end.to raise_error(NATS::Error) end # No error here since it fails synchronously expect(errors.count).to eql(0) expect(disconnects).to eql(0) expect(closes).to eql(0) end it 'should connect securely to server and authorize' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :ca_file => './spec/configs/certs/ca.pem', :verify_peer => true } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close future.resume(nc) end end end expect(errors.count).to eql(0) expect(messages.count).to eql(1) expect(disconnects).to eql(0) expect(closes).to eql(1) end it 'should give up connecting securely to server if cannot verify peer' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :ca_file => './spec/configs/certs/bad-ca.pem', :verify_peer => true } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close future.resume(nc) end end end expect(errors.count).to eql(2) expect(errors.first).to be_a(NATS::ConnectError) expect(errors.first.to_s).to eql('TLS Verification failed checking issuer based on CA ./spec/configs/certs/bad-ca.pem') expect(errors.last).to be_a(NATS::ConnectError) expect(messages.count).to eql(0) expect(disconnects).to eql(0) expect(closes).to eql(1) end it 'should connect securely with default TLS and protocols options and assume verify if CA given' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :ca_file => './spec/configs/certs/ca.pem' } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close end end end expect(messages.count).to eql(1) expect(errors.count).to eql(0) expect(closes).to eql(1) expect(reconnects).to eql(0) expect(disconnects).to eql(0) end it 'should connect securely with verify peer when multiple CAs are present in the CA file' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://secret:deadbeef@127.0.0.1:4445'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', :ca_file => './spec/configs/certs/multi-ca.pem', # First CA is invalid, second is valid :verify_peer => true } } nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://secret:deadbeef@127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") do nc.unsubscribe(sid) nc.close future.resume(nc) end end end expect(messages.count).to eql(1) expect(errors.count).to eql(0) expect(closes).to eql(1) expect(reconnects).to eql(0) expect(disconnects).to eql(0) end end context 'when servers require TLS' do before(:each) do @tls_server = NatsServerControl.new("nats://127.0.0.1:4445", '/tmp/test-nats-4445.pid', "-c ./spec/configs/tls-no-auth.conf") @tls_server.start_server @tls_server2 = NatsServerControl.new("nats://127.0.0.1:4446", '/tmp/test-nats-4446.pid', "-c ./spec/configs/tls-no-auth.conf") @tls_server2.start_server end after(:each) do @tls_server.kill_server if NATS.server_running?(@tls_server.uri) @tls_server2.kill_server if NATS.server_running?(@tls_server2.uri) end it 'should connect securely to server and reconnect' do errors = [] messages = [] connects = 0 reconnects = 0 disconnects = 0 closes = 0 with_em_timeout(10) do |future| NATS.on_error do |e| errors << e end NATS.on_disconnect do |e| disconnects += 1 end NATS.on_reconnect do reconnects += 1 end NATS.on_close do closes += 1 end options = { :servers => ['nats://127.0.0.1:4445','nats://127.0.0.1:4446'], :max_reconnect_attempts => 1, :dont_randomize_servers => true, :tls => { :ssl_version => :TLSv1_2, :protocols => [:tlsv1_2], :private_key_file => './spec/configs/certs/key.pem', :cert_chain_file => './spec/configs/certs/server.pem', } } # Confirm connect is ok nc = NATS.connect(options) do |conn| expect(conn.connected_server).to eql(URI.parse('nats://127.0.0.1:4445')) connects += 1 end sid = nc.subscribe("hello") do |msg| messages << msg end nc.flush do nc.publish("hello", "world") end # Confirm that the client reconnects EM.add_timer(1) do @tls_server.kill_server end # Should be reconnected at this point and subscriptions replayed EM.add_timer(2) do nc.flush do nc.publish("hello", "again") do nc.unsubscribe(sid) nc.close future.resume end end end end expect(errors.count).to eql(0) expect(messages.count).to eql(2) expect(disconnects).to eql(1) expect(closes).to eql(1) end end end ================================================ FILE: spec/client/cluster_auth_token_spec.rb ================================================ require 'spec_helper' require 'yaml' describe 'Client - auth token' do before(:all) do auth_options = { 'token' => 'deadbeef', 'timeout' => 5 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } nodes = [] configs = [s1_config_opts, s2_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { token: '#{auth_options["token"]}' timeout: #{auth_options["timeout"]} } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: #{auth_options["timeout"]} } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2 = nodes end before(:each) do [@s1, @s2].each do |s| s.start_server(true) end end after(:each) do [@s1, @s2].each do |s| s.kill_server end end let(:auth_token) { 'deadbeef' } it 'should properly connect to different servers using token' do EM.run do c1 = NATS.connect(:uri => @s1.uri, :token => auth_token) c2 = NATS.connect(:uri => "nats://#{auth_token}@#{@s1.uri.host}:#{@s1.uri.port}") c3 = NATS.connect("nats://#{auth_token}@#{@s1.uri.host}:#{@s1.uri.port}") wait_on_connections([c1, c2, c3]) do EM.stop end end end it 'should raise auth error when using wrong token' do errors = [] with_em_timeout(2) do |future| NATS.on_error do |e| errors << e future.resume end NATS.connect(:uri => @s1.uri, :token => 'wrong') end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::AuthError) errors = [] with_em_timeout(2) do |future| NATS.on_error do |e| errors << e future.resume end NATS.connect(:uri => "nats://wrong@#{@s1.uri.host}:#{@s1.uri.port}") end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::AuthError) errors = [] with_em_timeout(2) do |future| NATS.on_error do |e| errors << e future.resume end NATS.connect("nats://wrong@#{@s1.uri.host}:#{@s1.uri.port}") end expect(errors.count).to eql(1) expect(errors.first).to be_a(NATS::AuthError) end it 'should reuse token for reconnecting' do data = 'Hello World!' to_send = 100 received = c1_received = c2_received = 0 reconnected = false with_em_timeout(3) do c1 = NATS.connect(:uri => @s1.uri, :token => auth_token) c2 = NATS.connect(:uri => @s2.uri, :token => auth_token) c1.on_reconnect do reconnected = true end c1.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) received += 1 end c2.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) received += 1 end wait_on_routes_connected([c1, c2]) do (1..to_send).each { c2.publish('foo', data) } end EM.add_timer(0.5) do @s1.kill_server EM.add_timer(1) do (1..to_send).each { c2.publish('foo', data) } end end end expect(received).to eql(to_send*2) expect(reconnected).to eql(true) end end ================================================ FILE: spec/client/cluster_auto_discovery_spec.rb ================================================ require 'spec_helper' require 'yaml' describe 'Client - cluster auto discovery' do before(:each) do auth_options = { 'user' => 'secret', 'password' => 'user', 'token' => 'deadbeef', 'timeout' => 5 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } s3_config_opts = { 'pid_file' => '/tmp/nats_cluster_s3.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4244, 'cluster_port' => 6224 } s4_config_opts = { 'pid_file' => '/tmp/nats_cluster_s4.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4245, 'cluster_port' => 6225 } s5_config_opts = { 'pid_file' => '/tmp/nats_cluster_s5.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4246, 'cluster_port' => 6226 } nodes = [] configs = [s1_config_opts, s2_config_opts, s3_config_opts, s4_config_opts, s5_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { user: '#{auth_options["user"]}' password: '#{auth_options["password"]}' timeout: 5 } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: 5 } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2, @s3, @s4, @s5 = nodes end after(:each) do [@s1, @s2, @s3, @s4, @s5].each do |s| s.kill_server if NATS.server_running?(s.uri) end end it 'should properly discover nodes in cluster upon connect and randomize by default' do # Start servers and form a cluster, client will only be aware of first node # though it will discover the other nodes in the cluster automatically. [@s1, @s2, @s3].each do |node| node.start_server(true) end servers_upon_connect = 0 c1_msgs = [] randomize_calls = [] allow_any_instance_of(Array).to receive(:'shuffle!') do |srvs| randomize_calls << srvs end with_em_timeout do c1 = NATS.connect(:servers => [@s1.uri]) do |nats| servers_upon_connect = nats.server_pool.count expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) NATS.stop end c1.subscribe("hello") do |msg| c1_msgs << msg end end expect(servers_upon_connect).to eql(3) srvs = randomize_calls.last expect(srvs.count).to eql(2) srv_0 = srvs.select{ |srv| srv[:uri] == @s2.uri }.first srv_1 = srvs.select{ |srv| srv[:uri] == @s3.uri }.first expect(srv_0[:uri]).not_to be_nil expect(srv_1[:uri]).not_to be_nil end it 'should properly discover nodes in cluster eventually after first connect' do [@s1, @s2].each do |node| node.start_server(true) end servers_upon_connect = 0 servers_after_connect = 0 with_em_timeout(5) do c1 = NATS.connect(:servers => [@s1.uri]) do |nats| servers_upon_connect = nats.server_pool.count expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) @s3.start_server(true) EM.add_timer(2) do # Should have detected new server asynchronously servers_after_connect = nats.server_pool.count expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) expect(nats.discovered_servers.count).to eql(2) end end end expect(servers_upon_connect).to eql(2) expect(servers_after_connect).to eql(3) end it 'should properly discover nodes in cluster eventually' do [@s1, @s2, @s3, @s4, @s5].each do |s| s.start_server(true) end pool_a = nil pool_b = nil pool_c = nil c1_errors = [] c2_errors = [] c3_errors = [] with_em_timeout(5) do c1 = NATS.connect(:servers => [@s1.uri], :reconnect => false) do |nats| expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) EM.add_timer(2) do pool_a = nats.server_pool expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) c1.close end end c1.on_error do |e| c1_errors << e end c2 = NATS.connect(:servers => [@s1.uri], :reconnect => false, :dont_randomize_servers => false) do |nats| expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) EM.add_timer(2) do pool_b = nats.server_pool expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) c2.close end end c2.on_error do |e| c2_errors << e end c3 = NATS.connect(:servers => [@s1.uri], :reconnect => false) do |nats| expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) EM.add_timer(2) do pool_c = nats.server_pool expect(nats.connected_server).to eql(@s1.uri) expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) c3.close end end c3.on_error do |e| c3_errors << e end end expect(pool_a.count).to eql(5) expect(pool_b.count).to eql(5) expect(pool_c.count).to eql(5) expect(c1_errors.count).to eql(0) expect(c2_errors.count).to eql(0) expect(c3_errors.count).to eql(0) end it 'should properly discover nodes in cluster and reconnect to new one on failure' do [@s1, @s2].each do |node| node.start_server(true) end reconnects = [] servers_upon_connect = 0 servers_after_connect = 0 server_pool_state = nil with_em_timeout(10) do NATS.on_reconnect do |nats| reconnects << nats.connected_server server_pool_state = nats.server_pool end NATS.connect(:servers => [@s1.uri], :dont_randomize_servers => true) do |nats| servers_upon_connect = nats.server_pool.count expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) @s3.start_server(true) EM.add_timer(2) do # Should have detected new server asynchronously servers_after_connect = nats.server_pool.count expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) @s1.kill_server EM.add_timer(2) do @s1.start_server(true) @s2.kill_server end end end end expect(servers_upon_connect).to eql(2) expect(servers_after_connect).to eql(3) expect(reconnects.count).to eql(2) expect(reconnects.first.port).to eql(@s2.uri.port) expect(reconnects.last.port).to eql(@s3.uri.port) expect(server_pool_state.count).to eql(3) server_pool_state.each do |srv| expect(srv[:was_connected]).to eql(true) end end it 'should authenticate to discovered nodes using explicit credentials' do [@s1, @s2].each do |node| node.start_server(true) end reconnects = [] servers_upon_connect = 0 servers_after_connect = 0 server_pool_state = nil with_em_timeout(10) do NATS.on_reconnect do |nats| reconnects << nats.connected_server server_pool_state = nats.server_pool end NATS.connect({ :servers => ["nats://#{@s1.uri.host}:#{@s1.uri.port}"], :dont_randomize_servers => true, :user => @s1.uri.user, :pass => @s1.uri.password }) do |nats| servers_upon_connect = nats.server_pool.count expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) @s3.start_server(true) EM.add_timer(2) do # Should have detected new server asynchronously servers_after_connect = nats.server_pool.count expect(nats.server_pool.first[:uri]).to eql(nats.connected_server) @s1.kill_server EM.add_timer(2) do @s1.start_server(true) @s2.kill_server end end end end expect(servers_upon_connect).to eql(2) expect(servers_after_connect).to eql(3) expect(reconnects.count).to eql(2) expect(reconnects.first.port).to eql(@s2.uri.port) expect(reconnects.last.port).to eql(@s3.uri.port) expect(server_pool_state.count).to eql(3) server_pool_state.each do |srv| expect(srv[:was_connected]).to eql(true) end end end ================================================ FILE: spec/client/cluster_lb_spec.rb ================================================ require 'spec_helper' require 'uri' describe 'Client - cluster load balance' do before(:each) do auth_options = { 'user' => 'derek', 'password' => 'bella', 'token' => 'deadbeef', 'timeout' => 1 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } nodes = [] configs = [s1_config_opts, s2_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { user: '#{auth_options["user"]}' password: '#{auth_options["password"]}' timeout: 0.5 } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: 1 } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2 = nodes end before(:each) do [@s1, @s2].each do |s| s.start_server(true) end end after(:each) do [@s1, @s2].each do |s| s.kill_server end end # Tests that the randomize pool works correctly and that not # all clients are connecting to the same server. it 'should properly load balance between multiple servers with same client config' do clients = [] servers = { @s1.uri => 0, @s2.uri => 0 } EM.run do for i in 0...20 clients << NATS.connect(:uri => servers.keys) end results = {} wait_on_connections(clients) do clients.each do |c| servers[c.connected_server] += 1 port, ip = Socket.unpack_sockaddr_in(c.get_peername) expect(port).to eql(URI(c.connected_server).port) end servers.each_value do |v| expect(v > 0).to eql(true) end EM.stop end end end end ================================================ FILE: spec/client/cluster_multi_route_spec.rb ================================================ require 'spec_helper' describe 'Client - cluster' do before(:all) do auth_options = { 'user' => 'derek', 'password' => 'bella', 'token' => 'deadbeef', 'timeout' => 5 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } nodes = [] configs = [s1_config_opts, s2_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { user: '#{auth_options["user"]}' password: '#{auth_options["password"]}' timeout: #{auth_options["timeout"]} } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: #{auth_options["timeout"]} } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2 = nodes end before(:each) do [@s1, @s2].each do |s| s.start_server(true) unless NATS.server_running? s.uri end end after(:each) do [@s1, @s2].each do |s| s.kill_server end end it 'should properly route plain messages between different servers' do data = 'Hello World!' received = 0 with_em_timeout do c1 = NATS.connect(:uri => @s1.uri) do |nats| nats.subscribe('foo') do |msg| expect(msg).to eql(data) received += 1 end end c2 = NATS.connect(:uri => @s2.uri) do |nats| nats.subscribe('foo') do |msg| expect(msg).to eql(data) received += 1 end end c1.flush do c2.flush do wait_on_routes_connected([c1, c2]) do c2.publish('foo', data) c2.publish('foo', data) flush_routes([c1, c2]) { EM.stop } end end end end expect(received).to eql(4) end it 'should properly route messages with staggered startup' do @s2.kill_server data = 'Hello World!' received = 0 EM.run do c1 = NATS.connect(:uri => @s1.uri) do c1.subscribe('foo') do |msg| expect(msg).to eql(data) received += 1 end c1.flush do @s2.start_server c2 = NATS.connect(:uri => @s2.uri) do flush_routes([c1, c2]) do c2.publish('foo', data) c2.publish('foo', data) flush_routes([c1, c2]) { EM.stop } end end end end end expect(received).to eql(2) end end ================================================ FILE: spec/client/cluster_retry_connect_spec.rb ================================================ require 'spec_helper' describe 'Client - cluster retry connect' do before(:all) do auth_options = { 'user' => 'derek', 'password' => 'bella', 'token' => 'deadbeef', 'timeout' => 1 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } nodes = [] configs = [s1_config_opts, s2_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { user: '#{auth_options["user"]}' password: '#{auth_options["password"]}' timeout: 5 } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: 5 } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2 = nodes end before(:each) do [@s1, @s2].each do |s| s.start_server(true) end end after(:each) do [@s1, @s2].each do |s| s.kill_server end end it 'should re-establish asymmetric route connections upon restart' do data = 'Hello World!' received = 0 with_em_timeout(5) do |future| c1 = NATS.connect(:uri => @s1.uri) c2 = NATS.connect(:uri => @s2.uri) c1.subscribe('foo') do |msg| expect(msg).to eql(data) received += 1 if received == 2 future.resume # proper exit elsif received == 1 # Here we will kill s1, which does not actively connect to anyone. # Upon restart we will make sure the route was re-established properly. @s1.kill_server @s1.start_server wait_on_routes_connected([c1, c2]) do # Auto discovery kicks in so connects to next one available expect(c1.connected_server).to eql(@s2.uri) c2.publish('foo', data) end end end wait_on_routes_connected([c1, c2]) do c2.publish('foo', data) end end expect(received).to eql(2) end it 'should call the reconnect, disconnect, and close callbacks whenever it detaches from a server' do disconnects = [] reconnects = [] with_em_timeout(10) do |future| nc = NATS.connect(:servers => [@s1.uri, @s2.uri], :dont_randomize_servers => true, :max_reconnect_attempts => 3) do |nc| nc.on_disconnect do |reasons| disconnects << reasons end nc.on_reconnect do |nc| reconnects << nc.connected_server end nc.on_close do future.resume end EM.add_timer(1) do @s1.kill_server end EM.add_timer(2) do @s1.start_server @s2.kill_server end end nc.flush do nc.subscribe("hello") end end expect(disconnects.count).to eql(3) expect(reconnects.count).to eql(2) expect(disconnects[0].to_s).to eql(%Q(Client disconnected from server on #{@s1.uri})) expect(disconnects[1].to_s).to eql(%Q(Client disconnected from server on #{@s2.uri})) expect(disconnects[2].to_s).to eql(%Q(Client disconnected from server on #{@s1.uri})) expect(reconnects[0]).to eql(@s2.uri) expect(reconnects[1]).to eql(@s1.uri) end end ================================================ FILE: spec/client/cluster_spec.rb ================================================ require 'spec_helper' require 'yaml' describe 'Client - cluster' do before(:all) do auth_options = { 'user' => 'derek', 'password' => 'bella', 'token' => 'deadbeef', 'timeout' => 5 } s1_config_opts = { 'pid_file' => '/tmp/nats_cluster_s1.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4242, 'cluster_port' => 6222 } s2_config_opts = { 'pid_file' => '/tmp/nats_cluster_s2.pid', 'authorization' => auth_options, 'host' => '127.0.0.1', 'port' => 4243, 'cluster_port' => 6223 } nodes = [] configs = [s1_config_opts, s2_config_opts] configs.each do |config_opts| other_nodes_configs = configs.select do |conf| conf['cluster_port'] != config_opts['cluster_port'] end routes = [] other_nodes_configs.each do |conf| routes << "nats-route://foo:bar@127.0.0.1:#{conf['cluster_port']}" end nodes << NatsServerControl.init_with_config_from_string(%Q( host: '#{config_opts['host']}' port: #{config_opts['port']} pid_file: '#{config_opts['pid_file']}' authorization { user: '#{auth_options["user"]}' password: '#{auth_options["password"]}' timeout: #{auth_options["timeout"]} } cluster { host: '#{config_opts['host']}' port: #{config_opts['cluster_port']} authorization { user: foo password: bar timeout: #{auth_options["timeout"]} } routes = [ #{routes.join("\n ")} ] } ), config_opts) end @s1, @s2 = nodes end before(:each) do [@s1, @s2].each do |s| s.start_server(true) end end after(:each) do [@s1, @s2].each do |s| s.kill_server end end it 'should properly connect to different servers' do EM.run do c1 = NATS.connect(:uri => @s1.uri) c2 = NATS.connect(:uri => @s2.uri) wait_on_connections([c1, c2]) do EM.stop end end end it 'should properly route messages for distributed queues on different servers' do data = 'Hello World!' to_send = 100 received = c1_received = c2_received = 0 EM.run do c1 = NATS.connect(:uri => @s1.uri) c2 = NATS.connect(:uri => @s2.uri) c1.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) c1_received += 1 received += 1 end c2.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) c2_received += 1 received += 1 end wait_on_routes_connected([c1, c2]) do (1..to_send).each { c2.publish('foo', data) } flush_routes([c1, c2]) { EM.stop } end end expect(received).to eql(to_send) expect(c1_received < to_send).to eql(true) expect(c2_received < to_send).to eql(true) expect(c1_received).to be_within(25).of(to_send/2) expect(c2_received).to be_within(25).of(to_send/2) end it 'should properly route messages for distributed queues and normal subscribers on different servers' do data = 'Hello World!' to_send = 100 received = c1_received = c2_received = 0 EM.run do c1 = NATS.connect(:uri => @s1.uri) c2 = NATS.connect(:uri => @s2.uri) c1.subscribe('foo') do |msg| expect(msg).to eql(data) received += 1 end c1.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) c1_received += 1 received += 1 end c2.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) c2_received += 1 received += 1 end wait_on_routes_connected([c1, c2]) do (1..to_send).each { c2.publish('foo', data) } flush_routes([c1, c2]) { EM.stop } end end expect(received).to eql(to_send*2) # queue subscriber + normal subscriber expect(c1_received < to_send).to eql(true) expect(c2_received < to_send).to eql(true) expect(c1_received).to be_within(20).of(to_send/2) expect(c2_received).to be_within(20).of(to_send/2) end it 'should properly route messages for distributed queues with multiple groups on different servers' do data = 'Hello World!' to_send = 100 received = c1a_received = c2a_received = 0 c1b_received = c2b_received = 0 EM.run do c1 = NATS.connect(:uri => @s1.uri) c2 = NATS.connect(:uri => @s2.uri) c1.subscribe('foo') do |msg| expect(msg).to eql(data) received += 1 end c1.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) c1a_received += 1 received += 1 end c1.subscribe('foo', :queue => 'baz') do |msg| expect(msg).to eql(data) c1b_received += 1 received += 1 end c2.subscribe('foo', :queue => 'bar') do |msg| expect(msg).to eql(data) c2a_received += 1 received += 1 end c2.subscribe('foo', :queue => 'baz') do |msg| expect(msg).to eql(data) c2b_received += 1 received += 1 end wait_on_routes_connected([c1, c2]) do (1..to_send).each { c2.publish('foo', data) } (1..to_send).each { c1.publish('foo', data) } flush_routes([c1, c2]) { EM.stop } end end expect(received).to eql(to_send*6) # 2 queue subscribers + normal subscriber * 2 pub loops expect(c1a_received).to be_within(25).of(to_send) expect(c2a_received).to be_within(25).of(to_send) expect(c1b_received).to be_within(25).of(to_send) expect(c2b_received).to be_within(25).of(to_send) end it 'should properly route messages for distributed queues with reply subjects on different servers' do data = 'Hello World!' to_send = 100 received = c1_received = c2_received = 0 EM.run do c1 = NATS.connect(:uri => @s1.uri) c2 = NATS.connect(:uri => @s2.uri) c1.subscribe('foo', :queue => 'reply_test') do |msg| expect(msg).to eql(data) c1_received += 1 received += 1 end c2.subscribe('foo', :queue => 'reply_test') do |msg| expect(msg).to eql(data) c2_received += 1 received += 1 end wait_on_routes_connected([c1, c2]) do (1..to_send).each { c2.publish('foo', data, 'bar') } flush_routes([c1, c2]) { EM.stop } end end expect(received).to eql(to_send) expect(c1_received < to_send).to eql(true) expect(c2_received < to_send).to eql(true) expect(c1_received).to be_within(25).of(to_send/2) expect(c2_received).to be_within(25).of(to_send/2) end end ================================================ FILE: spec/client/error_on_client_spec.rb ================================================ require 'spec_helper' describe 'Client - error on client' do context 'NATS::ServerError' do it 'should show the contents of $1 when matched UNKNOWN' do EchoServer.start { expect(EM.reactor_running?).to eql(true) NATS.on_error{ |e| # We use a CONNECT request to match Unknown Protocol expect(e.to_s).to match(/\AUnknown Protocol: CONNECT.+/i) NATS.stop EchoServer.stop } # Send CONNECT request to the server. NATS.start(:uri => EchoServer::URI) } expect(NATS.connected?).to be_falsey expect(EM.reactor_running?).to be_falsey end it 'should disconnect when pings outstanding over limit' do nc = nil errors = [] closes = 0 SilentServer.start { expect(EM.reactor_running?).to eql(true) NATS.on_error do |e| errors << e NATS.stop EchoServer.stop end NATS.on_close do closes += 1 end nc = NATS.connect({ :servers => [SilentServer::URI], :max_outstanding_pings => 2, :ping_interval => 1, :reconnect => false }) expect(nc).to receive(:queue_server_rt).exactly(2).times } expect(nc.connected?).to be_falsey expect(EM.reactor_running?).to be_falsey expect(errors.first).to be_a(NATS::ConnectError) expect(closes).to eql(1) end end end ================================================ FILE: spec/client/fast_producer_spec.rb ================================================ require 'spec_helper' describe 'Client - fast producer' do before(:all) do @s = NatsServerControl.new @s.start_server end after(:all) do @s.kill_server end it 'should report the maximum outbound threshold' do expect(NATS::FAST_PRODUCER_THRESHOLD).to_not eql(nil) expect(NATS::FAST_PRODUCER_THRESHOLD).to eql(10*1024*1024) end it 'should report the outstanding bytes pending' do data = 'hello world!' proto = "PUB foo #{data.size}\r\n#{data}\r\n" NATS.start do (1..100).each { NATS.publish('foo', data) } expect(NATS.pending_data_size).to eql(100*proto.size) NATS.stop end end it 'should not raise an error when exceeding the threshold by default' do data = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' EM.run do nc = NATS.connect expect do # Put as many commands in pending as we can and confirm that # hitting the maximum threshold does not yield an error. while (nc.pending_data_size <= NATS::FAST_PRODUCER_THRESHOLD) do nc.publish('foo', data) end end.not_to raise_error nc.close EM.stop end end it 'should raise an error when exceeding the threshold if enabled' do data = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' EM.run do c = NATS.connect(:fast_producer_error => true) expect do while (c.pending_data_size <= NATS::FAST_PRODUCER_THRESHOLD) do c.publish('foo', data) end end.to raise_error(NATS::ClientError) c.close EM.stop end end context 'with NATS_FAST_PRODUCER enabled from ENV' do before(:all) do ENV['NATS_FAST_PRODUCER'] = 'true' end after(:all) do ENV.delete 'NATS_FAST_PRODUCER' end it 'should raise an error when exceeding the threshold' do data = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' EM.run do c = NATS.connect expect do while (c.pending_data_size <= NATS::FAST_PRODUCER_THRESHOLD) do c.publish('foo', data) end end.to raise_error(NATS::ClientError) c.close EM.stop end end end end ================================================ FILE: spec/client/nuid_spec.rb ================================================ # Copyright 2016-2018 The NATS Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'spec_helper' describe 'NUID' do it "should have a fixed length and be unique" do nuid = NATS::NUID.new entries = [] total = 500_000 total.times do entry = nuid.next expect(entry.size).to eql(NATS::NUID::TOTAL_LENGTH) entries << entry end entries.uniq! expect(entries.count).to eql(total) end it "should be unique after 1M entries" do total = 1_000_000 entries = [] nuid = NATS::NUID.new total.times do entries << nuid.next end entries.uniq! expect(entries.count).to eql(total) end it "should randomize the prefix after sequence is done" do nuid = NATS::NUID.new seq_a = nuid.instance_variable_get('@seq') inc_a = nuid.instance_variable_get('@inc') a = nuid.next seq_b = nuid.instance_variable_get('@seq') inc_b = nuid.instance_variable_get('@inc') expect(seq_a < seq_b).to eql(true) expect(seq_b).to eql(seq_a + inc_a) b = nuid.next nuid.instance_variable_set('@seq', NATS::NUID::MAX_SEQ+1) c = nuid.next l = NATS::NUID::PREFIX_LENGTH expect(a[0..l]).to eql(b[0..l]) expect(a[0..l]).to_not eql(c[0..l]) end end ================================================ FILE: spec/client/partial_message_spec.rb ================================================ require 'spec_helper' describe 'Client - partial message behavior' do before do @s = NatsServerControl.new @s.start_server end after do @s.kill_server end it 'should not hold stale message data across a reconnect' do got_message = false expect do with_em_timeout(5) do |future| # First client connects, and will attempt to reconnects c1 = NATS.connect(:uri => @s.uri, :reconnect_time_wait => 0.25) wait_on_connections([c1]) do c1.subscribe('subject') do |msg| got_message = true expect(msg).to eql('complete message') future.resume end # Client receives partial message before server terminates. c1.receive_data("MSG subject 2 32\r\nincomplete") # Server restarts, disconnecting the first client. @s.kill_server @s.start_server # One more client connects and publishes a message. NATS.connect(:uri => @s.uri) do |c2| EM.add_timer(0.50) do expect(c1.connected_server).to eql(@s.uri) expect(c2.connected_server).to eql(@s.uri) c1.flush { c2.publish('subject', 'complete message') } end end end end end.to_not raise_exception expect(got_message).to eql(true) end end ================================================ FILE: spec/client/queues_spec.rb ================================================ require 'spec_helper' describe "Client - queue group support" do before(:each) do @s = NatsServerControl.new @s.start_server end after(:each) do @s.kill_server end it "should deliver a message to only one subscriber in a queue group" do received = 0 NATS.start do s1 = NATS.subscribe('foo', :queue => 'g1') { received += 1 } expect(s1).to_not eql(nil) s2 = NATS.subscribe('foo', :queue => 'g1') { received += 1 } expect(s2).to_not eql(nil) NATS.publish('foo', 'hello') { NATS.stop } end expect(received).to eql(1) end it "should allow queue receivers and normal receivers to work together" do received = 0 NATS.start do (0...5).each { NATS.subscribe('foo', :queue => 'g1') { received += 1 } } NATS.subscribe('foo') { received += 1 } NATS.publish('foo', 'hello') { NATS.stop } end expect(received).to eql(2) end it "should spread messages equally across multiple receivers" do TOTAL = 1000 NUM_SUBSCRIBERS = 10 AVG = TOTAL / NUM_SUBSCRIBERS ALLOWED_V = TOTAL * 0.05 received = Hash.new(0) total = 0 NATS.start do (0...NUM_SUBSCRIBERS).each do |i| NATS.subscribe('foo.bar', :queue => 'queue_group_1') do received[i] = received[i] + 1 total += 1 end end (0...TOTAL).each { NATS.publish('foo.bar', 'ok') } NATS.flush { NATS.stop } end received.each_value do |count| expect((AVG - count).abs < ALLOWED_V).to eql(true) end expect(total).to eql(TOTAL) end it "should deliver a message to only one subscriber in a queue group, regardless of wildcard subjects" do received = 0 NATS.start do NATS.subscribe('foo.bar', :queue => 'g1') { received += 1 } NATS.subscribe('foo.*', :queue => 'g1') { received += 1 } NATS.subscribe('foo.>', :queue => 'g1') { received += 1 } NATS.publish('foo.bar', 'hello') { NATS.stop } end expect(received).to eql(1) end it "should deliver 1 message/group for each publish" do received_g1 = 0 received_g2 = 0 NATS.start do NATS.subscribe('foo.bar', :queue => 'g1') { received_g1 += 1 } 5.times do NATS.subscribe('foo.bar', :queue => 'g2') { received_g2 += 1 } end 10.times do NATS.publish('foo.bar', 'hello') end NATS.flush { NATS.stop } end expect(received_g1).to eql(10) expect(received_g2).to eql(10) end it "should re-establish queue groups on reconnect" do buckets = Hash.new(0) num_to_send = 1000 reconnect_sid = false reconnect_timer = nil NATS.start(:reconnect_time_wait => 0.25) do |conn| reconnect_sid = NATS.subscribe("reconnected_trigger") do NATS.publish("control", "reconnected") NATS.unsubscribe(reconnect_sid) EM.cancel_timer(reconnect_timer) end 2.times do |ii| NATS.subscribe("test_queue", :queue => "test_queue") do buckets[ii] += 1 NATS.publish("control", "ack") end end # Don't hang indefinitely EM.add_timer(30) { EM.stop } total_acked = 0 # Ensure the queue subscribes have been processed NATS.flush do NATS.subscribe("control") do |state| case state when "start" @s.kill_server @s.start_server reconnect_timer = EM.add_periodic_timer(0.25) do NATS.publish("reconnected_trigger") end when "reconnected" num_to_send.times { NATS.publish("test_queue") } when "ack" total_acked +=1 NATS.stop if total_acked == num_to_send else puts "Unexpected message: #{state}" NATS.stop end end end NATS.flush { NATS.publish("control", "start") } end # Messages are distributed uniformly at random to queue subscribers. This # forms a binomal distribution with a probability of success (receiving a # message) of .5 in the two subscriber case. This verifies that the receive # count of each subscriber is within 3 standard deviations of the mean. # In theory, this means that the test will fail ~ .3% of the time. buckets.values.each do |msg_count| expect(msg_count).to be_within(150).of(500) end end end ================================================ FILE: spec/client/reconnect_spec.rb ================================================ require 'spec_helper' describe 'Client - reconnect specification' do before(:all) do R_USER = 'derek' R_PASS = 'mypassword' R_TEST_AUTH_SERVER = "nats://#{R_USER}:#{R_PASS}@127.0.0.1:9333" R_TEST_SERVER_PID = '/tmp/nats_reconnect_authorization.pid' E_TEST_SERVER = "nats://127.0.0.1:9666" E_TEST_SERVER_PID = '/tmp/nats_reconnect_exception_test.pid' @as = NatsServerControl.new(R_TEST_AUTH_SERVER, R_TEST_SERVER_PID) @as.start_server @s = NatsServerControl.new @s.start_server @es = NatsServerControl.new(E_TEST_SERVER, E_TEST_SERVER_PID) @es.start_server end after(:all) do @s.kill_server @as.kill_server @es.kill_server end it 'should properly report connected after connect callback' do NATS.start do expect(NATS.connected?).to eql(true) expect(NATS.reconnecting?).to eql(false) NATS.stop end end it 'should do publish without error even if reconnected to an authorized server' do NATS.start(:uri => R_TEST_AUTH_SERVER, :reconnect_time_wait => 0.25) do |c| c.on_reconnect do expect do NATS.publish('reconnect test') end.to_not raise_error end @as.kill_server EM.add_timer(0.25) { @as.start_server } EM.add_timer(1.0) { NATS.stop } end end it 'should subscribe if reconnected' do received = false @as.kill_server EM.run do c = NATS.connect(:uri => R_TEST_AUTH_SERVER, :reconnect => true, :max_reconnect_attempts => -1, :reconnect_time_wait => 0.25) c.subscribe('foo') { |msg| received = true } @as.start_server EM.add_periodic_timer(0.1) { c.publish('foo', 'xxx') } EM.add_timer(1) { NATS.stop EM.stop } end expect(received).to eql(true) end it 'should not get stuck reconnecting due to uncaught exceptions' do received = false @as.kill_server class SomeException < StandardError; end expect do EM.run do NATS.connect(:uri => R_TEST_AUTH_SERVER, :reconnect => true, :max_reconnect_attempts => -1, :reconnect_time_wait => 0.25) raise SomeException.new end end.to raise_error(SomeException) end it 'should back off trying to reconnect' do @as.start_server disconnected_time = nil reconnected_time = nil connected_once = false EM.run do NATS.on_disconnect do # Capture the time of the first disconnect disconnected_time ||= NATS::MonotonicTime.now end NATS.on_reconnect do reconnected_time ||= NATS::MonotonicTime.now end NATS.connect(:uri => R_TEST_AUTH_SERVER, :reconnect => true, :max_reconnect_attempts => -1, :reconnect_time_wait => 2) do connected_once = true end EM.add_timer(0.5) do @as.kill_server end EM.add_timer(1) do @as.start_server end EM.add_timer(3) do NATS.stop EM.stop end end expect(connected_once).to eql(true) expect(disconnected_time).to_not be(nil) expect(reconnected_time).to_not be(nil) expect(reconnected_time - disconnected_time >= 2).to eql(true) end end ================================================ FILE: spec/client/server_info_spec.rb ================================================ require 'spec_helper' describe 'Client - server_info support' do before(:all) do @s = NatsServerControl.new @s.start_server end after(:all) do @s.kill_server end it 'should report nil when not connected for server_info' do expect(NATS.connected?).to eql(false) expect(NATS.server_info).to eql(nil) end it 'should report the appropriate server_info for connected clients' do NATS.start do info = NATS.server_info expect(info).to_not eql(nil) expect(info).to be_a Hash expect(info).to have_key :server_id expect(info).to have_key :version expect(info).to have_key :proto expect(info).to have_key :client_id expect(info).to have_key :max_payload expect(info).to have_key :host expect(info).to have_key :port NATS.stop end end it 'should report server info for individual connections' do NATS.start do info = NATS.client.server_info expect(info).to_not be(nil) expect(info).to be_a Hash expect(info).to have_key :server_id expect(info).to have_key :version expect(info).to have_key :proto expect(info).to have_key :client_id expect(info).to have_key :max_payload expect(info).to have_key :host expect(info).to have_key :port NATS.stop end end end ================================================ FILE: spec/client/sub_timeouts_spec.rb ================================================ require 'spec_helper' describe 'Client - subscriptions with timeouts' do before(:all) do TIMEOUT = 0.1 WAIT = 0.2 @s = NatsServerControl.new @s.start_server end after(:all) do @s.kill_server end it "a subscription should not receive a message after a timeout" do received = 0 NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT) EM.add_timer(WAIT) { NATS.publish('foo') { NATS.stop } } end expect(received).to eql(0) end it "a subscription should call the timeout callback if no messages are received" do received = 0 timeout_recvd = false NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT) { timeout_recvd = true } EM.add_timer(WAIT) { NATS.stop } end expect(timeout_recvd).to eql(true) expect(received).to eql(0) end it "a subscription should call the timeout callback if no messages are received, connection version" do received = 0 timeout_recvd = false NATS.start do |c| sid = c.subscribe('foo') { received += 1 } c.timeout(sid, TIMEOUT) { timeout_recvd = true } EM.add_timer(WAIT) { NATS.stop } end expect(timeout_recvd).to eql(true) expect(received).to eql(0) end it "a subscription should not call the timeout callback if a message is received" do received = 0 timeout_recvd = false NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT) { timeout_recvd = true } NATS.publish('foo') NATS.publish('foo') EM.add_timer(WAIT) { NATS.stop } end expect(timeout_recvd).to eql(false) expect(received).to eql(2) end it "a subscription should not call the timeout callback if a correct # messages are received" do received = 0 timeout_recvd = false NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT, :expected => 2) { timeout_recvd = true } NATS.publish('foo') NATS.publish('foo') EM.add_timer(WAIT) { NATS.stop } end expect(timeout_recvd).to eql(false) expect(received).to eql(2) end it "a subscription should call the timeout callback if a correct # messages are not received" do received = 0 timeout_recvd = false NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT, :expected => 2) { timeout_recvd = true } NATS.publish('foo') EM.add_timer(WAIT) { NATS.publish('foo') { NATS.stop} } end expect(timeout_recvd).to eql(true) expect(received).to eql(1) end it "a subscription should call the timeout callback and message callback if requested" do received = 0 timeout_recvd = false NATS.start do sid = NATS.subscribe('foo') { received += 1 } NATS.timeout(sid, TIMEOUT, :auto_unsubscribe => false) { timeout_recvd = true } EM.add_timer(WAIT) { NATS.publish('foo') { NATS.stop} } end expect(timeout_recvd).to eql(true) expect(received).to eql(1) end end ================================================ FILE: spec/configs/certs/bad-ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIGjzCCBHegAwIBAgIJAKT2W9SKY7o4MA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD VQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzAR BgNVBAoTCkFwY2VyYSBJbmMxEDAOBgNVBAsTB25hdHMuaW8xEjAQBgNVBAMTCWxv Y2FsaG9zdDEcMBoGCSqGSIb3DQEJARYNZGVyZWtAbmF0cy5pbzAeFw0xNTExMDUy MzA2MTdaFw0xOTExMDQyMzA2MTdaMIGLMQswCQYDVQQGEwJVUzELMAkGA1UECBMC Q0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzARBgNVBAoTCkFwY2VyYSBJbmMx EDAOBgNVBAsTB25hdHMuaW8xEjAQBgNVBAMTCWxvY2FsaG9zdDEcMBoGCSqGSIb3 DQEJARYNZGVyZWtAbmF0cy5pbzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC ggIBAJOyBvFaREbmO/yaw8UD8u5vSk+Qrwdkfa0iHMo11nkcVtynHNKcgRUTkZBC xEZILVsuPa+WSUcUc0ej0TmuimrtOjXGn+LD0TrDVz6dd6lBufLXjo1fbUnKUjml TBYB2h7StDksrBPFnbEOVKN+qb1No4YxfvbJ6EK3xfnsm3dvamnetJugrmQ2EUlu glPNZDIShu9Fcsiq2hjw+dJ2Erl8kx2/PE8nOdcDG9I4wAM71pw9L1dHGmMOnTsq opLDVkMNjeIgMPxj5aIhvS8Tcnj16ZNi4h10587vld8fIdz+OgTDFMNi91PgZQmX 9puXraBGi5UEn0ly57IIY+aFkx74jPWgnVYz8w8G+W2GTFYQEVgHcPTJ4aIPjyRd m/cLelV34TMNCoTXmPIKVBkJY01t2awUYN0AcauhmD1L+ihY2lVk330lxQR11ZQ/ rjSRpG6jzb6diVK5wpNjsRRt5zJgZr6BMp0LYwJESGjt0sF0zZxixvHu8EctVle4 zX6NHDic7mf4Wvo4rfnUyCGr7Y3OxB2vakq1fDZ1Di9OzpW/k8i/TE+mPRI5GTZt lR+c8mBxdV595EKHDxj0gY7PCM3Pe35p3oScWtfbpesTX6a7IL801ZwKKtN+4DOV mZhwiefztb/9IFPNXiuQnNh7mf7W2ob7SiGYct8iCLLjT64DAgMBAAGjgfMwgfAw HQYDVR0OBBYEFPDMEiYb7Np2STbm8j9qNj1aAvz2MIHABgNVHSMEgbgwgbWAFPDM EiYb7Np2STbm8j9qNj1aAvz2oYGRpIGOMIGLMQswCQYDVQQGEwJVUzELMAkGA1UE CBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzARBgNVBAoTCkFwY2VyYSBJ bmMxEDAOBgNVBAsTB25hdHMuaW8xEjAQBgNVBAMTCWxvY2FsaG9zdDEcMBoGCSqG SIb3DQEJARYNZGVyZWtAbmF0cy5pb4IJAKT2W9SKY7o4MAwGA1UdEwQFMAMBAf8w DQYJKoZIhvcNAQELBQADggIBAIkoO+svWiudydr4sQNv/XhDvH0GiWMjaI738fAB sGUKWXarXM9rsRtoQ78iwEBZmusEv0fmJ9hX275aZdduTJt4AnCBVptnSyMJS6K5 RZF4ZQ3zqT3QOeWepLqszqRZHf+xNfl9JiXZc3pqNhoh1YXPubCgY+TY1XFSrL+u Wmbs3n56Cede5+dKwMpT9SfQ7nL1pwKihx16vlBGTjjvJ0RE5Tx+0VRcDgbtIF52 pNlvjg9DL+UqP3S1WR0PcsUss/ygiC1NDegZr+I/04/wEG9Drwk1yPSshWsH90W0 7TmLDoWf5caAX62jOJtXbsA9JZ16RnIWy2iZYwg4YdE0rEeMbnDzrRucbyBahMX0 mKc8C+rroW0TRTrqxYDQTE5gmAghCa9EixcwSTgMH/U6zsRbbY62m9WA5fKfu3n0 z82+c36ijScHLgppTVosq+kkr/YE84ct56RMsg9esEKTxGxje812OSdHp/i2RzqW J59yo7KUn1nX7HsFvBVh9D8147J5BxtPztc0GtCQTXFT73nQapJjAd5J+AC5AB4t ShE+MRD+XIlPB/aMgtzz9Th8UCktVKoPOpFMC0SvFbbINWL/JO1QGhuZLMTKLjQN QBzjrETAOA9PICpI5hcPtTXz172X+I8/tIEFrZfew0Fdt/oAVcnb659zKiR8EuAq +Svp -----END CERTIFICATE----- ================================================ FILE: spec/configs/certs/ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIDXDCCAkQCCQDI2Vsry8+BDDANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJV UzELMAkGA1UECAwCQ0ExEDAOBgNVBAoMB1N5bmFkaWExEDAOBgNVBAsMB25hdHMu aW8xEjAQBgNVBAMMCWxvY2FsaG9zdDEcMBoGCSqGSIb3DQEJARYNZGVyZWtAbmF0 cy5pbzAeFw0xOTEwMTcxMzAzNThaFw0yOTEwMTQxMzAzNThaMHAxCzAJBgNVBAYT AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwHbmF0 cy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJla0Bu YXRzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAohX2dXdHIDM5 yZDWk96b0mwRTHhBIOKtMPTTs/zKmlAgjjDxW7kSg0JimTNds9YbJ33FhcEJKXtV KH3Cn0uyZPS1VcTzPr7XP2QI+9SqqLuahkHAhgqoRwK62fTFJgzdZO0f9w9WwzMi gGk/v7KkKFa/9xKLCa9DTEJ9FA34HuYoBxXMZvypDm8d+0kxOCdThpzhKeucE4ya jFlvOP9/l7GyjlczzAD/nt/QhPfSeIx1MF0ICj5qzwPD/jB1ekoL9OShoHvoEyXo UO13GMdVmZqwJcS7Vk5XNEZoH0cxSw/SrZGCE9SFjR1t8TAe3QZiZ9E8EAg4IzJQ jfR2II5LiQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBIwib+0xLth/1+URtgQFn8 dvQNqnJjlqC27U48qiTCTC5vJWbQDqUg9o6gtwZyYEHQ7dMmn68ozDzcGTCxaikV n01Bj2ijODK96Jrm/P5aVkP5Cn06FfudluZI2Q/A1cqTsa8V4rj02PpwCcLEaDqX yhztlhbKypWrlGuWpVlDBWstyRar98vvRK1XEyBu2NHp2fy49cwJCub4Cmz920fh oiIwzXIKtfnf1GEjUnsuFPMgCxvhjirYNPWWjqaBldrM/dBJqwTyZf/p6g40vufN JJDc65c4tyRwBSBdFn+Q4zD44M0AR/8THAeIfsT42lyl8fMV5A4fe1nAVJDC4Z/H -----END CERTIFICATE----- ================================================ FILE: spec/configs/certs/client-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDQjCCAiqgAwIBAgIJAJCSLX9jr5WzMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwH bmF0cy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJl a0BuYXRzLmlvMB4XDTE5MTAxNzEzMjI0MloXDTI5MTAxNDEzMjI0MlowDTELMAkG A1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsnD6dO3oS VoV4yt+c/Ax+XvJPIjNGgThT16clj9fuFhPiZ0mI9pSZ8Kmm2/56F8nj3zFzcThw OpYemXtdB+Nj5Oi/mfc9XCf1tzcp2u6CgADUyNMbNg2L04qbjhKhTQzFIvhWO2oa ++k9CB4Tf1VuLmWTmpBUA20N5kTW98DX2OHHHsKbo26I8XxYCKKfE8xbuREsHSNv Oq5Hmg9qzuWANAnm4/12Ss9aGLucxcF0SWd3G7oohjGm/BKvSoUbc1v01kL/DBxJ 5zHyWioezYfLIv9wHEjtuuC+8Lye4NxZ26V0JVizYQT2MyhrByVgD3KTFmyfsK1K GPeeKR63YTQXAgMBAAGjQjBAMCkGA1UdEQQiMCCCCWxvY2FsaG9zdIcEfwAAAYEN ZGVyZWtAbmF0cy5pbzATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQUF AAOCAQEAfGUnzmpXXigAJxcnVKQy1ago+GGOAGldlDKIcHoofkYibhWWrrojulHF pRPRVKm2S/P4rRnSsjrPfpf6I2Icd+oVdVxrsWcN5itbul8Xymsjl2gMSJSHknYs wTYNjdM4opRioArK69aRa26xXlxRs8YpRErF8Nb5mkxgvtUgtM8t/T/28MBprc7x 7NuYvohKlOcWbgdBYI+e3CA2XLRG/A+9EmOe8g66vW/uY0eaiWduBJSwXhd+stjg elXYnK+EEUpJIK9DeS7r6k6HreNZ2FPM90RxdbMP7Q+i3bJwic4cJG3QOdLl+IqK tME8kUPD/63mEDHHMJjgAktgaFX4bQ== -----END CERTIFICATE----- ================================================ FILE: spec/configs/certs/client-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCsnD6dO3oSVoV4 yt+c/Ax+XvJPIjNGgThT16clj9fuFhPiZ0mI9pSZ8Kmm2/56F8nj3zFzcThwOpYe mXtdB+Nj5Oi/mfc9XCf1tzcp2u6CgADUyNMbNg2L04qbjhKhTQzFIvhWO2oa++k9 CB4Tf1VuLmWTmpBUA20N5kTW98DX2OHHHsKbo26I8XxYCKKfE8xbuREsHSNvOq5H mg9qzuWANAnm4/12Ss9aGLucxcF0SWd3G7oohjGm/BKvSoUbc1v01kL/DBxJ5zHy WioezYfLIv9wHEjtuuC+8Lye4NxZ26V0JVizYQT2MyhrByVgD3KTFmyfsK1KGPee KR63YTQXAgMBAAECggEBAKc6FHt2NPTxOAxn2C6aDmycBftesfiblnu8EWaVrmgu oYMV+CsmYZ+mhmZu+mNFCsam5JzoUvp/+BKbNeZSjx2nl0qRmvOqhdhLcbkuLybl ZmjAS64wNv2Bq+a6xRfaswWGtLuugkS0TCph4+mV0qmVb7mJ5ExQqWXu8kCl9QHn uKacp1wVFok9rmEI+byL1+Z01feKrkf/hcF6dk62U7zHNPajViJFTDww7hiHyfUH 6qsxIe1UWSNKtE61haEHkzqbDIDAy79jX4t3JobLToeVNCbJ7BSPf2IQSPJxELVL sidIJhndEjsbDR2CLpIF/EjsiSIaP7jh2zC9fxFpgSkCgYEA1qH0PH1JD5FqRV/p n9COYa6EifvSymGo4u/2FHgtX7wNSIQvqAVXenrQs41mz9E65womeqFXT/AZglaM 1PEjjwcFlDuLvUEYYJNgdXrIC515ZXS6TdvJ0JpQJLx28GzZ7h31tZXfwn68C3/i UGEHp+nN1BfBBQnsqvmGFFvHZFUCgYEAzeDlZHHijBlgHU+kGzKm7atJfAGsrv6/ tw7CIMEsL+z/y7pl3nwDLdZF+mLIvGuKlwIRajEzbYcEuVymCyG2/SmPMQEUf6j+ C1OmorX9CW8OwHmVCajkIgKn0ICFsF9iFv6aYZmm1kG48AIuYiQ7HOvY/MlilqFs 1p8sw6ZpQrsCgYEAj7Z9fQs+omfxymYAXnwc+hcKtAGkENL3bIzULryRVSrrkgTA jDaXbnFR0Qf7MWedkxnezfm+Js5TpkwhnGuiLaC8AZclaCFwGypTShZeYDifEmno XT2vkjfhNdfjo/Ser6vr3BxwaSDG9MQ6Wyu9HpeUtFD7c05D4++T8YnKpskCgYEA pCkcoIAStcWSFy0m3K0B3+dBvAiVyh/FfNDeyEFf24Mt4CPsEIBwBH+j4ugbyeoy YwC6JCPBLyeHA8q1d5DVmX4m+Fs1HioBD8UOzRUyA/CzIZSQ21f5OIlHiIDCmQUl cNJpBUQAfT2AmpgSphzfqcsBhWeLHjLvVx8rEYLC0fsCgYAiHdPZ3C0f7rWZP93N gY4DuldiO4d+KVsWAdBxeNgPznisUI7/ZZ/9NvCxGvA5NynyZr0qlpiKzVvtFJG8 1ZPUuFFRMAaWn9h5C+CwMPgk65tFC6lw/el0hpmcocSXVdiJEbkV0rnv9iGh0CYX HMACGrYlyZdDYM0CH/JAM+K/QQ== -----END PRIVATE KEY----- ================================================ FILE: spec/configs/certs/key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDm+0dlzcmiLa+L zdVqeVQ8B1/rWnErK+VvvjH7FmVodg5Z5+RXyojpd9ZBrVd6QrLSVMQPfFvBvGGX 4yI6Ph5KXUefa31vNOOMhp2FGSmaEVhETKGQ0xRh4VfaAerOP5Cunl0TbSyJyjkV a7aeMtcqTEiFL7Ae2EtiMhTrMrYpBDQ8rzm2i1IyTb9DX5v7DUOmrSynQSlVyXCz tRVGNL/kHlItpEku1SHt/AD3ogu8EgqQZFB8xRRw9fubYgh4Q0kx80e4k9QtTKnc F3B2NGb/ZcE5Z+mmHIBq8J2zKMijOrdd3m5TbQmzDbETEOjs4L1eoZRLcL/cvYu5 gmXdr4F7AgMBAAECggEBAK4sr3MiEbjcsHJAvXyzjwRRH1Bu+8VtLW7swe2vvrpd w4aiKXrV/BXpSsRtvPgxkXyvdMSkpuBZeFI7cVTwAJFc86RQPt77x9bwr5ltFwTZ rXCbRH3b3ZPNhByds3zhS+2Q92itu5cPyanQdn2mor9/lHPyOOGZgobCcynELL6R wRElkeDyf5ODuWEd7ADC5IFyZuwb3azNVexIK+0yqnMmv+QzEW3hsycFmFGAeB7v MIMjb2BhLrRr6Y5Nh+k58yM5DCf9h/OJhDpeXwLkxyK4BFg+aZffEbUX0wHDMR7f /nMv1g6cKvDWiLU8xLzez4t2qNIBNdxw5ZSLyQRRolECgYEA+ySTKrBAqI0Uwn8H sUFH95WhWUXryeRyGyQsnWAjZGF1+d67sSY2un2W6gfZrxRgiNLWEFq9AaUs0MuH 6syF4Xwx/aZgU/gvsGtkgzuKw1bgvekT9pS/+opmHRCZyQAFEHj0IEpzyB6rW1u/ LdlR3ShEENnmXilFv/uF/uXP5tMCgYEA63LiT0w46aGPA/E+aLRWU10c1eZ7KdhR c3En6zfgIxgFs8J38oLdkOR0CF6T53DSuvGR/OprVKdlnUhhDxBgT1oQjK2GlhPx JV5uMvarJDJxAwsF+7T4H2QtZ00BtEfpyp790+TlypSG1jo/BnSMmX2uEbV722lY hzINLY49obkCgYBEpN2YyG4T4+PtuXznxRkfogVk+kiVeVx68KtFJLbnw//UGT4i EHjbBmLOevDT+vTb0QzzkWmh3nzeYRM4aUiatjCPzP79VJPsW54whIDMHZ32KpPr TQMgPt3kSdpO5zN7KiRIAzGcXE2n/e7GYGUQ1uWr2XMu/4byD5SzdCscQwJ/Ymii LoKtRvk/zWYHr7uwWSeR5dVvpQ3E/XtONAImrIRd3cRqXfJUqTrTRKxDJXkCmyBc 5FkWg0t0LUkTSDiQCJqcUDA3EINFR1kwthxja72pfpwc5Be/nV9BmuuUysVD8myB qw8A/KsXsHKn5QrRuVXOa5hvLEXbuqYw29mX6QKBgDGDzIzpR9uPtBCqzWJmc+IJ z4m/1NFlEz0N0QNwZ/TlhyT60ytJNcmW8qkgOSTHG7RDueEIzjQ8LKJYH7kXjfcF 6AJczUG5PQo9cdJKo9JP3e1037P/58JpLcLe8xxQ4ce03zZpzhsxR2G/tz8DstJs b8jpnLyqfGrcV2feUtIZ -----END PRIVATE KEY----- ================================================ FILE: spec/configs/certs/multi-ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIDSTCCAjGgAwIBAgIQDbspQLZaH4pjQlTk/trkBzANBgkqhkiG9w0BAQsFADBO MQwwCgYDVQQGEwNVU0ExFjAUBgNVBAoTDUNsb3VkIEZvdW5kcnkxJjAkBgNVBAMT HWRlZmF1bHQubmF0cy1jYS5ib3NoLWludGVybmFsMB4XDTE4MDcyMDE0NTg1NFoX DTE5MDcyMDE0NTg1NFowTjEMMAoGA1UEBhMDVVNBMRYwFAYDVQQKEw1DbG91ZCBG b3VuZHJ5MSYwJAYDVQQDEx1kZWZhdWx0Lm5hdHMtY2EuYm9zaC1pbnRlcm5hbDCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCtx6M8UOqd7X2XxVnuap4B ZK2kHVG3xi7hmJzCsPm2JW29mmF2G62SBlZ+LmvnrOhU9gm0yzYE60jn3T2du7qe FUWJhI0VMGFwLJIgDXeroknuH5jrzRJHTZ5bXgDmY7e55gYBgxJ0HOD5kFV9f+iG 3yt9mHbyEN9EEN/d0AlY7a+9Pid/mGaifWtYLAMhT+13DGS8kVhkWaCEVPFL7rfQ 3DJ/mvgfKAGAdx9eNvAS+0sGnFkCvxXbhavBUk2krAf+Fo6WtxwxLb/rXvWRt4n7 zWLbKEXO4y+csBis6DaLZ25jqkV623kxrbLz1rKtA00GVO5f9Y+71v8vBkyweD8C AwEAAaMjMCEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI hvcNAQELBQADggEBAJ+3nPKz9VrS57Gxbrzocam4FqNokCxmk+9BaNeIWx5p8Vpb Vhq6Hh44rljWExEMdgISSQeN3vJrcWUe2u0NbdLCVPtn+D02cVYD9gGHwuRH5Nd6 DEzKLW5BqFBHxgcwpc97tEfOey+CduNRZbKbrZCYpLud+9NZR36xc0g4zno53HyG EBpywVUP0MKchQshl3Wjv50aoqThpaXWnVGd5hOrGWipWt8FUR6gkqYGfJh+sV4L 5QbsrD5B2AM3BPcyFsUlsfFKPHKR3WMZThjZ3GklRc8niYo1N9Rp6wo4rMpz6Jjc QAQiG4TMMMJs8jw8/fU8P4bt0K1TOz7Cmr6xa7c= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDXDCCAkQCCQDI2Vsry8+BDDANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJV UzELMAkGA1UECAwCQ0ExEDAOBgNVBAoMB1N5bmFkaWExEDAOBgNVBAsMB25hdHMu aW8xEjAQBgNVBAMMCWxvY2FsaG9zdDEcMBoGCSqGSIb3DQEJARYNZGVyZWtAbmF0 cy5pbzAeFw0xOTEwMTcxMzAzNThaFw0yOTEwMTQxMzAzNThaMHAxCzAJBgNVBAYT AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwHbmF0 cy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJla0Bu YXRzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAohX2dXdHIDM5 yZDWk96b0mwRTHhBIOKtMPTTs/zKmlAgjjDxW7kSg0JimTNds9YbJ33FhcEJKXtV KH3Cn0uyZPS1VcTzPr7XP2QI+9SqqLuahkHAhgqoRwK62fTFJgzdZO0f9w9WwzMi gGk/v7KkKFa/9xKLCa9DTEJ9FA34HuYoBxXMZvypDm8d+0kxOCdThpzhKeucE4ya jFlvOP9/l7GyjlczzAD/nt/QhPfSeIx1MF0ICj5qzwPD/jB1ekoL9OShoHvoEyXo UO13GMdVmZqwJcS7Vk5XNEZoH0cxSw/SrZGCE9SFjR1t8TAe3QZiZ9E8EAg4IzJQ jfR2II5LiQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBIwib+0xLth/1+URtgQFn8 dvQNqnJjlqC27U48qiTCTC5vJWbQDqUg9o6gtwZyYEHQ7dMmn68ozDzcGTCxaikV n01Bj2ijODK96Jrm/P5aVkP5Cn06FfudluZI2Q/A1cqTsa8V4rj02PpwCcLEaDqX yhztlhbKypWrlGuWpVlDBWstyRar98vvRK1XEyBu2NHp2fy49cwJCub4Cmz920fh oiIwzXIKtfnf1GEjUnsuFPMgCxvhjirYNPWWjqaBldrM/dBJqwTyZf/p6g40vufN JJDc65c4tyRwBSBdFn+Q4zD44M0AR/8THAeIfsT42lyl8fMV5A4fe1nAVJDC4Z/H -----END CERTIFICATE----- ================================================ FILE: spec/configs/certs/server.pem ================================================ -----BEGIN CERTIFICATE----- MIIDPTCCAiWgAwIBAgIJAJCSLX9jr5W7MA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV BAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UECwwH bmF0cy5pbzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg1kZXJl a0BuYXRzLmlvMB4XDTE5MTAxNzEzNTcyNloXDTI5MTAxNDEzNTcyNlowDTELMAkG A1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm+0dlzcmi La+LzdVqeVQ8B1/rWnErK+VvvjH7FmVodg5Z5+RXyojpd9ZBrVd6QrLSVMQPfFvB vGGX4yI6Ph5KXUefa31vNOOMhp2FGSmaEVhETKGQ0xRh4VfaAerOP5Cunl0TbSyJ yjkVa7aeMtcqTEiFL7Ae2EtiMhTrMrYpBDQ8rzm2i1IyTb9DX5v7DUOmrSynQSlV yXCztRVGNL/kHlItpEku1SHt/AD3ogu8EgqQZFB8xRRw9fubYgh4Q0kx80e4k9Qt TKncF3B2NGb/ZcE5Z+mmHIBq8J2zKMijOrdd3m5TbQmzDbETEOjs4L1eoZRLcL/c vYu5gmXdr4F7AgMBAAGjPTA7MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAd BgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQADggEB ADQYaEjWlOb9YzUnFGjfDC06dRZjRmK8TW/4GiDHIDk5TyZ1ROtskvyhVyTZJ5Vs qXOKJwpps0jK2edtrvZ7xIGw+Y41oPgYYhr5TK2c+oi2UOHG4BXqRbuwz/5cU+nM ZWOG1OrHBCbrMSeFsn7rzETnd8SZnw6ZE7LI62WstdoCY0lvNfjNv3kY/6hpPm+9 0bVzurZ28pdJ6YEJYgbPcOvxSzGDXTw9LaKEmqknTsrBKI2qm+myVTbRTimojYTo rw/xjHESAue/HkpOwWnFTOiTT+V4hZnDXygiSy+LWKP4zLnYOtsn0lN9OmD0z+aa gpoVMSncu2jMIDZX63IkQII= -----END CERTIFICATE----- ================================================ FILE: spec/configs/nkeys/foo-user.creds ================================================ -----BEGIN NATS USER JWT----- eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXTURGT1dHV1JGWkRGRFVSM0dPUkdESEtUTTdDUlZBVDQ1RkRFMllNRUY1N0VOQ0JBVFFRIiwiaWF0IjoxNTUzODQwOTQ0LCJpc3MiOiJBRDdTRUFOUzZCQ0JGNkZISUI3U1EzVUdKVlBXNTNCWE9BTFA3NVlYSkJCWFFMN0VBRkI2TkpOQSIsIm5hbWUiOiJmb28tdXNlciIsInN1YiI6IlVDSzVON042Nk9CT0lORlhBWUMyQUNKUVlGU09ENFZZTlU2QVBFSlRBVkZaQjJTVkhMS0dFVzdMIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.Vri09BN561m37GvuSWoGN9L9TSkwQbjC_jIv1BCJcoxZqNc_Pa7WbR12b3SAS4_Ip2D9-2HCwyYib1JUEIO8Bg ------END NATS USER JWT------ ************************* IMPORTANT ************************* NKEY Seed printed below can be used to sign and prove identity. NKEYs are sensitive and should be treated as secrets. -----BEGIN USER NKEY SEED----- SUAMLK2ZNL35WSMW37E7UD4VZ7ELPKW7DHC3BWBSD2GCZ7IUQQXZIORRBU ------END USER NKEY SEED------ ************************************************************* ================================================ FILE: spec/configs/nkeys/foo-user.jwt ================================================ eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJXTURGT1dHV1JGWkRGRFVSM0dPUkdESEtUTTdDUlZBVDQ1RkRFMllNRUY1N0VOQ0JBVFFRIiwiaWF0IjoxNTUzODQwOTQ0LCJpc3MiOiJBRDdTRUFOUzZCQ0JGNkZISUI3U1EzVUdKVlBXNTNCWE9BTFA3NVlYSkJCWFFMN0VBRkI2TkpOQSIsIm5hbWUiOiJmb28tdXNlciIsInN1YiI6IlVDSzVON042Nk9CT0lORlhBWUMyQUNKUVlGU09ENFZZTlU2QVBFSlRBVkZaQjJTVkhMS0dFVzdMIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.Vri09BN561m37GvuSWoGN9L9TSkwQbjC_jIv1BCJcoxZqNc_Pa7WbR12b3SAS4_Ip2D9-2HCwyYib1JUEIO8Bg ================================================ FILE: spec/configs/nkeys/foo-user.nk ================================================ SUAMLK2ZNL35WSMW37E7UD4VZ7ELPKW7DHC3BWBSD2GCZ7IUQQXZIORRBU ================================================ FILE: spec/configs/nkeys/op.jwt ================================================ -----BEGIN TEST OPERATOR JWT----- eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiIyTVhXQ0VaRFo2S0g3NU5ZU1NaUFBTTElET1NEVzdMVVE0RVRUSjJMWEhKQlFGWTRTMkZBIiwiaWF0IjoxNTUzODQwMzM3LCJpc3MiOiJPRFdJSUU3SjdOT1M3M1dWQk5WWTdIQ1dYVTRXWFdEQlNDVjRWSUtNNVk0TFhUT1Q1U1FQT0xXTCIsIm5hbWUiOiJ0ZXN0X29wZXJhdG9yIiwic3ViIjoiT0RXSUlFN0o3Tk9TNzNXVkJOVlk3SENXWFU0V1hXREJTQ1Y0VklLTTVZNExYVE9UNVNRUE9MV0wiLCJ0eXBlIjoib3BlcmF0b3IifQ.V_v6k3aMOpyau83RaqNHW_YmZw8X0ZnJWLOas3YvQYIyXrHF0bL9inBaQw6zXzbN_ViQnNskhB7tM40qguitAg ------END TEST OPERATOR JWT------ ================================================ FILE: spec/configs/tls-no-auth.conf ================================================ # Simple TLS config file port: 4444 net: "127.0.0.1" tls { cert_file: "./spec/configs/certs/server.pem" key_file: "./spec/configs/certs/key.pem" timeout: 5 } ================================================ FILE: spec/configs/tls.conf ================================================ # Simple TLS config file port: 4443 net: "127.0.0.1" tls { cert_file: "./spec/configs/certs/server.pem" key_file: "./spec/configs/certs/key.pem" timeout: 5 } authorization { user: secret password: deadbeef timeout: 5 } ================================================ FILE: spec/configs/tlsverify.conf ================================================ # Simple TLS config file port: 4445 net: "127.0.0.1" tls { cert_file: "./spec/configs/certs/server.pem" key_file: "./spec/configs/certs/key.pem" timeout: 5 # Optional certificate authority for clients ca_file: "./spec/configs/certs/ca.pem" # Require a client certificate verify: true } ================================================ FILE: spec/server/max_connections_spec.rb ================================================ require 'spec_helper' require 'yaml' describe 'Server - max connections support' do before (:all) do MC_SERVER_PID = '/tmp/nats_mc_pid.pid' MC_SERVER = 'nats://localhost:9272' MC_LOG_FILE = '/tmp/nats_mc_test.log' MC_CONFIG = File.dirname(__FILE__) + '/resources/max_connections.yml' MC_FLAGS = "-c #{MC_CONFIG}" FileUtils.rm_f(MC_LOG_FILE) @s = RubyNatsServerControl.new(MC_SERVER, MC_SERVER_PID, MC_FLAGS) @s.start_server end after(:all) do @s.kill_server NATS.server_running?(MC_SERVER).should be_falsey FileUtils.rm_f(MC_LOG_FILE) end it 'should not allow connections above the maximum allowed' do config = File.open(MC_CONFIG) { |f| YAML.load(f) } max = config['max_connections'] err_received = false conns = [] EM.run do (1..max).each { conns << NATS.connect({:uri => MC_SERVER}) } wait_on_connections(conns) do c = NATS.connect({:uri => MC_SERVER}) EM.add_timer(0.25) { NATS.stop } c.on_error do |err| err_received = true err.should be_an_instance_of NATS::ServerError c.close conns.each { |c| c.close } EM.stop end end end err_received.should be_truthy end end ================================================ FILE: spec/server/monitor_spec.rb ================================================ require 'spec_helper' require 'fileutils' require 'net/http' require 'uri' require 'nats/server/server' require 'nats/server/options' require 'nats/server/const' require 'nats/server/util' describe 'Server - monitor' do before (:all) do HTTP_SERVER_PID = '/tmp/nats_http.pid' HTTP_SERVER = 'nats://127.0.0.1:9229' LOG_FILE = '/tmp/nats_http.log' HTTP_PORT = 9230 HTTP_FLAGS = "-m #{HTTP_PORT} -l #{LOG_FILE}" @rs = RubyNatsServerControl.new(HTTP_SERVER, HTTP_SERVER_PID, HTTP_FLAGS) @rs.start_server end after(:all) do @rs.kill_server FileUtils.rm_f(LOG_FILE) end it 'should process simple command line arguments for http port' do NATSD::Server.process_options('-m 4222'.split) opts = NATSD::Server.options opts[:http_port].should == 4222 end it 'should process long command line arguments for http port' do NATSD::Server.process_options('--http 4222'.split) opts = NATSD::Server.options opts[:http_port].should == 4222 end it 'should properly parse http port from config file' do config_file = File.dirname(__FILE__) + '/resources/monitor.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options opts[:http_net].should == '127.0.0.1' opts[:http_port].should == 4222 opts[:http_user].should == 'derek' opts[:http_password].should == 'foo' end it 'should start monitor http servers when requested' do total_wait, now = 0, Time.now begin s = TCPSocket.open(NATSD::Server.host, HTTP_PORT) rescue Errno::ECONNREFUSED => e total_wait = Time.now - now now = Time.now retry unless total_wait > 5 ensure s.close if s end end it 'should return \'ok\' for /healthz' do host, port = NATSD::Server.host, HTTP_PORT healthz_req = Net::HTTP::Get.new("/healthz") healthz_resp = Net::HTTP.new(host, port).start { |http| http.request(healthz_req) } healthz_resp.body.should =~ /ok/i end it 'should return varz with proper members' do host, port = NATSD::Server.host, HTTP_PORT varz_req = Net::HTTP::Get.new("/varz") varz_resp = Net::HTTP.new(host, port).start { |http| http.request(varz_req) } varz_resp.body.should_not be_nil varz = JSON.parse(varz_resp.body, :symbolize_keys => true, :symbolize_names => true) varz.should be_an_instance_of Hash varz.should have_key :start varz.should have_key :options varz.should have_key :mem varz.should have_key :cpu varz.should have_key :cores varz.should have_key :connections varz.should have_key :routes varz.should have_key :in_msgs varz.should have_key :in_bytes varz.should have_key :out_msgs varz.should have_key :out_bytes # Check to make sure we pick up cores correctly varz[:cores].should > 0 end it 'should properly track number of connections' do EM.run do conns = [] (1..10).each { conns << NATS.connect(:uri => HTTP_SERVER) } # Wait for them to register and varz to allow updates wait_on_connections(conns) do host, port = NATSD::Server.host, HTTP_PORT varz_req = Net::HTTP::Get.new("/varz") varz_resp = Net::HTTP.new(host, port).start { |http| http.request(varz_req) } varz_resp.body.should_not be_nil varz = JSON.parse(varz_resp.body, :symbolize_keys => true, :symbolize_names => true) varz[:connections].should == 10 conns.each { |c| c.close } EM.stop end end end it 'should track inbound and outbound message counts and bytes' do NATS.start(:uri => HTTP_SERVER) do NATS.subscribe('foo') NATS.subscribe('foo') (1..11).each { NATS.publish('foo', 'hello world') } NATS.flush do host, port = NATSD::Server.host, HTTP_PORT varz_req = Net::HTTP::Get.new("/varz") varz_resp = Net::HTTP.new(host, port).start { |http| http.request(varz_req) } varz_resp.body.should_not be_nil varz = JSON.parse(varz_resp.body, :symbolize_keys => true, :symbolize_names => true) varz[:in_msgs].should == 11 varz[:out_msgs].should == 22 varz[:in_bytes].should == 121 varz[:out_bytes].should == 242 NATS.stop end end end it 'should return connz with proper members' do EM.run do conns = [] (1..10).each { conns << NATS.connect(:uri => HTTP_SERVER) } # Wait for them to register and connz to allow updates wait_on_connections(conns) do host, port = NATSD::Server.host, HTTP_PORT connz_req = Net::HTTP::Get.new("/connz") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 10 connz[:connections].size.should == 10 c_info = connz[:connections].first c_info.should have_key :cid c_info.should have_key :ip c_info.should have_key :port c_info.should have_key :subscriptions c_info.should have_key :pending_size c_info.should have_key :in_msgs c_info.should have_key :out_msgs c_info.should have_key :in_bytes c_info.should have_key :out_bytes conns.each { |c| c.close } EM.stop end end end it 'should return connz with subset of connections if requested' do EM.run do conns = [] (1..50).each { conns << NATS.connect(:uri => HTTP_SERVER) } # Wait for them to register and connz to allow updates wait_on_connections(conns) do host, port = NATSD::Server.host, HTTP_PORT connz_req = Net::HTTP::Get.new("/connz?n=11") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 50 connz[:connections].size.should == 11 c_info = connz[:connections].first c_info.should have_key :cid c_info.should have_key :ip c_info.should have_key :port c_info.should have_key :subscriptions c_info.should have_key :pending_size c_info.should have_key :in_msgs c_info.should have_key :out_msgs c_info.should have_key :in_bytes c_info.should have_key :out_bytes conns.each { |c| c.close } EM.stop end end end it 'should return connz with subset of connections sorted correctly if requested' do EM.run do conns = [] (1..10).each do conns << NATS.connect(:uri => HTTP_SERVER) end (1..4).each do conns << c = NATS.connect(:uri => HTTP_SERVER) c.subscribe('foo') c.subscribe('foo') end conns << c = NATS.connect(:uri => HTTP_SERVER) (1..10).each { c.publish('foo', "hello world") } # Wait for them to register and connz to allow updates wait_on_connections(conns) do host, port = NATSD::Server.host, HTTP_PORT # Test different sorts # out_msgs connz_req = Net::HTTP::Get.new("/connz?n=4&s=out_msgs") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 4 connz[:connections].each do |c_info| c_info[:out_msgs].should == 20 end # msgs_to connz_req = Net::HTTP::Get.new("/connz?n=4&s=msgs_to") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 4 connz[:connections].each do |c_info| c_info[:out_msgs].should == 20 end # out_bytes connz_req = Net::HTTP::Get.new("/connz?n=2&s=out_bytes") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 2 connz[:connections].each do |c_info| c_info[:out_bytes].should == 220 end # bytes_to connz_req = Net::HTTP::Get.new("/connz?n=2&s=bytes_to") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 2 connz[:connections].each do |c_info| c_info[:out_bytes].should == 220 end # in_msgs connz_req = Net::HTTP::Get.new("/connz?n=1&s=in_msgs") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 1 c_info = connz[:connections].first c_info[:in_msgs].should == 10 # msgs_from connz_req = Net::HTTP::Get.new("/connz?n=1&s=msgs_from") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 1 c_info = connz[:connections].first c_info[:in_msgs].should == 10 # in_bytes connz_req = Net::HTTP::Get.new("/connz?n=1&s=in_bytes") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 1 c_info = connz[:connections].first c_info[:in_bytes].should == 110 # bytes_from connz_req = Net::HTTP::Get.new("/connz?n=1&s=bytes_from") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 1 c_info = connz[:connections].first c_info[:in_bytes].should == 110 # subscriptions (short form) connz_req = Net::HTTP::Get.new("/connz?n=1&s=subs") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 1 c_info = connz[:connections].first c_info[:subscriptions].should == 2 # subscriptions (long form) connz_req = Net::HTTP::Get.new("/connz?n=1&s=subscriptions") connz_resp = Net::HTTP.new(host, port).start { |http| http.request(connz_req) } connz_resp.body.should_not be_nil connz = JSON.parse(connz_resp.body, :symbolize_keys => true, :symbolize_names => true) connz.should have_key :pending_size connz.should have_key :num_connections connz[:num_connections].should == 15 connz[:connections].size.should == 1 c_info = connz[:connections].first c_info[:subscriptions].should == 2 conns.each { |c| c.close } EM.stop end end end it 'should require auth if configured to do so' do config_file = File.dirname(__FILE__) + '/resources/monitor.yml' config = File.open(config_file) { |f| YAML.load(f) } uri = "nats://#{config['net']}:#{config['port']}" auth_s = RubyNatsServerControl.new(uri, config['pid_file'], "-c #{config_file}") auth_s.start_server host, port = config['http']['net'], config['http']['port'] begin sleep(0.5) s = TCPSocket.open(host, port) ensure s.close if s end varz_req = Net::HTTP::Get.new("/varz") varz_resp = Net::HTTP.new(host, port).start { |http| http.request(varz_req) } varz_resp.code.should_not == '200' varz_resp.body.should be_empty # Do proper auth here varz_req.basic_auth(config['http']['user'], config['http']['password']) varz_resp = Net::HTTP.new(host, port).start { |http| http.request(varz_req) } varz_resp.code.should == '200' varz_resp.body.should_not be_empty varz = JSON.parse(varz_resp.body, :symbolize_keys => true, :symbolize_names => true) varz.should be_an_instance_of Hash varz.should have_key :start varz.should have_key :options varz.should have_key :mem varz.should have_key :cpu varz.should have_key :cores varz.should have_key :connections varz.should have_key :in_msgs varz.should have_key :in_bytes varz.should have_key :out_msgs varz.should have_key :out_bytes auth_s.kill_server if auth_s end end ================================================ FILE: spec/server/multi_user_auth_spec.rb ================================================ require 'spec_helper' require 'fileutils' require 'nats/server/server' require "nats/server/sublist" require "nats/server/options" require "nats/server/const" require "nats/server/util" describe 'Server - multi-user authorization' do before (:all) do config_file = File.dirname(__FILE__) + '/resources/multi_user_auth.yml' @config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) @opts = NATSD::Server.options @log_file = @config['log_file'] @host = @config['net'] @port = @config['port'] @uri = "nats://#{@host}:#{@port}" @s = RubyNatsServerControl.new(@uri, @config['pid_file'], "-c #{config_file}") @s.start_server end after (:all) do @s.kill_server FileUtils.rm_f(@log_file) end it 'should have a users option array even with only a single user defined' do config_file = File.dirname(__FILE__) + '/resources/auth.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options user = config['authorization']['user'] pass = config['authorization']['pass'] opts[:user].should == user opts[:pass].should == pass opts[:users].should_not be_nil opts[:users].should be_an_instance_of Array first = opts[:users].first first[:user].should == user first[:pass].should == pass end it 'should have a users array when user passed in on command line' do user = 'derek' pass = 'foo' NATSD::Server.process_options("--user #{user} --pass #{pass}".split) opts = NATSD::Server.options opts[:user].should == user opts[:pass].should == pass opts[:users].should_not be_nil opts[:users].should be_an_instance_of Array first = opts[:users].first first[:user].should == user first[:pass].should == pass end it 'should support mixed auth models and report singleton correctly' do config_file = File.dirname(__FILE__) + '/resources/mixed_auth.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options user = config['authorization']['user'] pass = config['authorization']['pass'] opts[:user].should == user opts[:pass].should == pass opts[:users].should_not be_nil opts[:users].should be_an_instance_of Array first = opts[:users].first first[:user].should == user first[:pass].should == pass end it 'should accept pass or password in multi form' do config_file = File.dirname(__FILE__) + '/resources/multi_user_auth_long.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options opts[:users].should_not be_nil opts[:users].should be_an_instance_of Array opts[:users].each { |u| u[:pass].should_not be_nil } end it 'should report first auth user as main user for backward compatability' do first = @opts[:users].first @opts[:user].should == first[:user] @opts[:pass].should == first[:pass] end it 'should not allow unauthorized access with multi auth' do expect do NATS.start(:uri => @uri) { NATS.stop } end.to raise_error NATS::Error end it 'should allow multi-users to be configured for auth' do @opts[:users].length.should >= 3 user1 = @opts[:users][0][:user] pass1 = @opts[:users][0][:pass] user2 = @opts[:users][1][:user] pass2 = @opts[:users][1][:pass] user3 = @opts[:users][2][:user] pass3 = @opts[:users][2][:pass] expect do uri = "nats://#{user1}:#{pass1}@#{@host}:#{@port}" NATS.start(:uri => uri) { NATS.stop } end.to_not raise_error expect do uri = "nats://#{user2}:#{pass2}@#{@host}:#{@port}" NATS.start(:uri => uri) { NATS.stop } end.to_not raise_error expect do uri = "nats://#{user3}:#{pass3}@#{@host}:#{@port}" NATS.start(:uri => uri) { NATS.stop } end.to_not raise_error end end ================================================ FILE: spec/server/protocol_spec.rb ================================================ require 'spec_helper' require 'nats/server/const' describe 'Server - NATS Protocol' do context 'sub' do it 'should match simple sub' do str = "SUB foo 1\r\n" NATSD::SUB_OP =~ str $1.should == 'foo' $3.should == nil $4.should == '1' end it 'should not care about case' do str = "sub foo 1\r\n" NATSD::SUB_OP =~ str $1.should == 'foo' $3.should == nil $4.should == '1' end it 'should match a queue group' do str = "SUB foo bargroup 1\r\n" NATSD::SUB_OP =~ str $1.should == 'foo' $3.should == 'bargroup' $4.should == '1' end it 'should not care about extra spaces' do str = "SUB foo bargroup 1\r\n" NATSD::SUB_OP =~ str $1.should == 'foo' $3.should == 'bargroup' $4.should == '1' end it 'should care about extra spaces at end' do str = "SUB foo bargroup 1 \r\n" (NATSD::SUB_OP =~ str).should be_falsey end it 'should properly match first one when multiple present' do str = "SUB foo 1\r\nSUB bar 2\r\nSUB baz 3\r\n" NATSD::SUB_OP =~ str $1.should == 'foo' $3.should == nil $4.should == '1' $'.should == "SUB bar 2\r\nSUB baz 3\r\n" end it 'should not tolerate spaces after \r\n' do str = "SUB foo 1\r\n SUB bar 2\r\nSUB baz 3\r\n" NATSD::SUB_OP =~ str str = $' NATSD::SUB_OP =~ str $1.should == nil $3.should == nil $4.should == nil end end context 'unsub' do it 'should process simple unsub' do str = "UNSUB 1\r\n" NATSD::UNSUB_OP =~ str $1.should == '1' end it 'should not care about case' do str = "unsub 1\r\n" NATSD::UNSUB_OP =~ str $1.should == '1' end it 'should tolerate extra spaces' do str = "UNSUB 1\r\n" NATSD::UNSUB_OP =~ str $1.should == '1' end it 'should properly match first one when multiple present' do str = "UNSUB 1\r\nUNSUB 2\r\nSUB foo 2\r\n" NATSD::UNSUB_OP =~ str $1.should == '1' $'.should == "UNSUB 2\r\nSUB foo 2\r\n" end it 'should properly parse auto-unsubscribe message count' do str = "UNSUB 1 22\r\n" NATSD::UNSUB_OP =~ str $1.should == '1' $2.should be $3.to_i.should == 22 end it 'should properly parse auto-unsubscribe message count with extra spaces' do str = "UNSUB 1 22\r\n" NATSD::UNSUB_OP =~ str $1.should == '1' $2.should be $3.to_i.should == 22 end end context 'pub' do it 'should process simple pub' do str = "PUB foo 2\r\nok\r\n" NATSD::PUB_OP =~ str $1.should == 'foo' $3.should == nil $4.to_i.should == 2 $'.should == "ok\r\n" end it 'should not care about case' do str = "pub foo 2\r\nok\r\n" NATSD::PUB_OP =~ str $1.should == 'foo' $3.should == nil $4.to_i.should == 2 $'.should == "ok\r\n" end it 'should process reply semantics' do str = "PUB foo bar 2\r\nok\r\n" NATSD::PUB_OP =~ str $1.should == 'foo' $3.should == 'bar' $4.to_i.should == 2 $'.should == "ok\r\n" end it 'should not care about extra spaces' do str = "PUB foo bar 2\r\nok\r\n" NATSD::PUB_OP =~ str $1.should == 'foo' $3.should == 'bar' $4.to_i.should == 2 $'.should == "ok\r\n" end it 'should care about extra spaces at end' do str = "PUB foo bar 2 \r\nok\r\n" (NATSD::PUB_OP =~ str).should be_falsey end it 'should properly match first one when multiple present' do str = "PUB foo bar 2\r\nok\r\nPUB bar 11\r\nHello World\r\n" NATSD::PUB_OP =~ str $1.should == 'foo' $3.should == 'bar' $4.to_i.should == 2 $'.should == "ok\r\nPUB bar 11\r\nHello World\r\n" end end context 'misc' do it 'should process ping requests' do str = "PING\r\n" (NATSD::PING =~ str).should be_truthy end it 'should process ping requests with spaces' do str = "PING \r\n" (NATSD::PING =~ str).should be_truthy end it 'should process pong responses' do str = "PONG\r\n" (NATSD::PONG =~ str).should be_truthy end it 'should process ping responses with spaces' do str = "PONG \r\n" (NATSD::PONG =~ str).should be_truthy end it 'should process info requests' do str = "INFO\r\n" (NATSD::INFO =~ str).should be_truthy end it 'should process info requests with spaces' do str = "INFO \r\n" (NATSD::INFO =~ str).should be_truthy end it 'should process connect requests' do str = "CONNECT {\"user\":\"derek\"}\r\n" NATSD::CONNECT =~ str $1.should == "{\"user\":\"derek\"}" end end context 'mixed' do it 'should process multiple commands in one buffer properly' do str = "PUB foo bar 2\r\nok\r\nSUB bar 22\r\nPUB bar 11\r\nHello World\r\n" NATSD::PUB_OP =~ str $1.should == 'foo' $3.should == 'bar' $4.to_i.should == 2 $'.should == "ok\r\nSUB bar 22\r\nPUB bar 11\r\nHello World\r\n" str = $' str = str.slice($4.to_i + NATSD::CR_LF_SIZE, str.bytesize) NATSD::SUB_OP =~ str $1.should == 'bar' $3.should == nil $4.should == '22' end end context 'client' do it 'should process ping and pong responsess' do str = "PING\r\n" (NATS::PING =~ str).should be_truthy str = "PING \r\n" (NATS::PING =~ str).should be_truthy str = "PONG\r\n" (NATS::PONG =~ str).should be_truthy str = "PONG \r\n" (NATS::PONG =~ str).should be_truthy end it 'should process ok responses' do str = "+OK\r\n" (NATS::OK =~ str).should be_truthy str = "+OK \r\n" (NATS::OK =~ str).should be_truthy end it 'should process err responses' do str = "-ERR 'string too long'\r\n" (NATS::ERR =~ str).should be_truthy $1.should == "'string too long'" end it 'should process messages' do str = "MSG foo 2 11\r\nHello World\r\n" NATS::MSG =~ str $1.should == 'foo' $2.should == '2' $4.should == nil $5.to_i.should == 11 $'.should == "Hello World\r\n" end it 'should process messages with a reply' do str = "MSG foo 2 reply_to_me 11\r\nHello World\r\n" NATS::MSG =~ str $1.should == 'foo' $2.should == '2' $4.should == 'reply_to_me' $5.to_i.should == 11 $'.should == "Hello World\r\n" end it 'should process messages with extra spaces' do str = "MSG foo 2 reply_to_me 11\r\nHello World\r\n" NATS::MSG =~ str $1.should == 'foo' $2.should == '2' $4.should == 'reply_to_me' $5.to_i.should == 11 $'.should == "Hello World\r\n" end it 'should process multiple messages in a single read properly' do str = "MSG foo 2 11\r\nHello World\r\nMSG foo 2 reply_to_me 2\r\nok\r\n" NATS::MSG =~ str $1.should == 'foo' $2.should == '2' $4.should == nil $5.to_i.should == 11 str = $' str = str.slice($5.to_i + NATSD::CR_LF_SIZE, str.bytesize) NATS::MSG =~ str $1.should == 'foo' $2.should == '2' $4.should == 'reply_to_me' $5.to_i.should == 2 $'.should == "ok\r\n" end end end ================================================ FILE: spec/server/resources/auth.yml ================================================ --- port: 4242 net: localhost authorization: user: derek pass: foo ================================================ FILE: spec/server/resources/b1_cluster.yml ================================================ port: 6242 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. cluster: port: 6254 authorization: user: route_user password: cafebabe timeout: 1 # These are actively connected from this server. Other servers # can connect to us if they supply the correct credentials from # above. routes: - nats-route://route_user:cafebabe@127.0.0.1:6256 - nats-route://route_user:cafebabe@127.0.0.1:6256 pid_file: '/tmp/nats_cluster_b1.pid' log_file: '/tmp/nats_cluster_b1.log' # Debug Options logtime: true debug: true trace: true ================================================ FILE: spec/server/resources/b2_cluster.yml ================================================ port: 6244 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. cluster: port: 6256 authorization: user: route_user password: cafebabe timeout: 1 # These are actively connected from this server. Other servers # can connect to us if they supply the correct credentials from # above. routes: - nats-route://route_user:cafebabe@127.0.0.1:6254 pid_file: '/tmp/nats_cluster_b2.pid' log_file: '/tmp/nats_cluster_b2.log' # Debug Options logtime: true debug: true trace: true ================================================ FILE: spec/server/resources/cluster.yml ================================================ --- # # Sample Server Sonfiguration # nats-server -c ./cluster.yml # port: 4242 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. # # NATS can support both full mesh and directed-acyclic # graph setups. Its up to the configuration setup to avoid cycles. # # The port definition allows us to receive incoming connections. # Comment out if you want to suppress incoming connections. # # The server can solicit active connections via the routes definitions below. # # authorization is similar to client connection definitions. cluster: port: 4244 authorization: user: route_user password: cafebabe token: deadbeef timeout: 1 # These are actively connected from this server. Other servers # can connect to us if they supply the correct credentials from # above. routes: - nats-route://foo:bar@127.0.0.1:4220 - nats-route://foo:bar@127.0.0.1:4221 pid_file: '/tmp/nats_test.pid' # log_file: '/tmp/nats_test.log' # Debug Options logtime: true debug: false trace: false # Protocol/Limits max_control_line: 512 max_payload: 512000 max_pending: 2000000 # EM/IO no_epoll: false no_kqueue: true ================================================ FILE: spec/server/resources/config.yml ================================================ --- port: 4242 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 pid_file: '/tmp/nats_test.pid' log_file: '/tmp/nats_test.log' ssl: false # Debug Options logtime: true debug: false trace: false # Protocol/Limits max_control_line: 512 max_payload: 512000 max_pending: 2000000 max_connections: 128 # EM/IO no_epoll: false no_kqueue: true ================================================ FILE: spec/server/resources/max_connections.yml ================================================ --- port: 9272 net: localhost # Protocol/Limits max_connections: 32 ================================================ FILE: spec/server/resources/mixed_auth.yml ================================================ --- port: 4242 net: localhost authorization: user: derek pass: foo users: - user: sam pass: wrestling - user: meg pass: soccer ================================================ FILE: spec/server/resources/monitor.yml ================================================ --- port: 4242 net: localhost http: net: 127.0.0.1 port: 4222 user: derek password: foo pid_file: '/tmp/nats_test.pid' log_file: '/tmp/nats_test.log' # Debug Options logtime: true debug: false trace: false # Protocol/Limits max_control_line: 512 max_payload: 512000 max_pending: 2000000 # EM/IO no_epoll: false no_kqueue: true ================================================ FILE: spec/server/resources/multi_user_auth.yml ================================================ --- port: 9226 net: localhost authorization: users: - user: derek pass: foo - user: sam pass: wrestling - user: meg pass: soccer timeout: 1 pid_file: '/tmp/nats_multi_user_auth_test.pid' log_file: '/tmp/nats_multi_user_auth_test.log' # Debug Options logtime: true debug: false trace: false ================================================ FILE: spec/server/resources/multi_user_auth_long.yml ================================================ --- port: 9226 net: localhost authorization: users: - user: derek pass: foo - user: sam password: wrestling - user: meg password: soccer timeout: 1 pid_file: '/tmp/nats_multi_user_auth_test.pid' log_file: '/tmp/nats_multi_user_auth_test.log' # Debug Options logtime: true debug: false trace: false ================================================ FILE: spec/server/resources/ping.yml ================================================ --- port: 2421 net: localhost pid_file: '/tmp/nats_ping_test.pid' log_file: '/tmp/nats_ping_test.log' # Ping options (in secs) ping: interval: 0.1 max_outstanding: 2 # Debug Options logtime: true debug: false trace: false # Protocol/Limits max_control_line: 512 max_payload: 512000 max_pending: 2000000 # EM/IO no_epoll: false no_kqueue: true ================================================ FILE: spec/server/resources/s1_cluster.yml ================================================ --- # # Sample Server Sonfiguration # nats-server -c ./s[N]_cluster.yml # port: 4242 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. cluster: port: 4262 authorization: user: ruser password: cafebabe token: deadbeef timeout: 1 pid_file: '/tmp/nats_cluster_s1.pid' #log_file: '/tmp/nats_cluster_s1.log' # Debug Options logtime: true debug: false trace: false ================================================ FILE: spec/server/resources/s2_cluster.yml ================================================ --- # # Sample Server Sonfiguration # nats-server -c ./s[N]_cluster.yml # port: 4244 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. cluster: port: 4264 authorization: user: ruser password: cafebabe token: deadbeef timeout: 1 routes: - nats-route://ruser:cafebabe@localhost:4262 # Connect to S1 pid_file: '/tmp/nats_cluster_s2.pid' #log_file: '/tmp/nats_cluster_s2.log' # Debug Options logtime: true debug: false trace: false ================================================ FILE: spec/server/resources/s3_cluster.yml ================================================ --- # # Sample Server Sonfiguration # nats-server -c ./s[N]_cluster.yml # port: 4246 net: localhost authorization: user: derek password: bella token: deadbeef timeout: 1 # This is the cluster definition for NATS. cluster: port: 4266 authorization: user: ruser password: cafebabe token: deadbeef timeout: 1 routes: - nats-route://ruser:cafebabe@127.0.0.1:4262 # Connect to s1 pid_file: '/tmp/nats_cluster_s3.pid' #log_file: '/tmp/nats_cluster_s3.log' # Debug Options logtime: true debug: false trace: false ================================================ FILE: spec/server/server_cluster_config_spec.rb ================================================ require 'spec_helper' require 'nats/server/server' require 'nats/server/sublist' require 'nats/server/options' require 'nats/server/const' require 'nats/server/util' require 'logger' describe 'Server - cluster configuration' do it 'should allow the cluster listen port to be set on command line' do NATSD::Server.process_options('-a localhost -p 5222 -r 8222'.split) opts = NATSD::Server.options opts[:addr].should == 'localhost' opts[:port].should == 5222 opts[:cluster_port].should == 8222 end it 'should allow the cluster listen port to be set on command line (long form)' do NATSD::Server.process_options('-a localhost -p 5222 --cluster_port 8222'.split) opts = NATSD::Server.options opts[:addr].should == 'localhost' opts[:port].should == 5222 opts[:cluster_port].should == 8222 end it 'should properly parse a config file' do config_file = File.dirname(__FILE__) + '/resources/cluster.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options opts[:config_file].should == config_file opts[:port].should == config['port'] opts[:addr].should == config['net'] opts[:user].should == config['authorization']['user'] opts[:pass].should == config['authorization']['password'] opts[:token].should == config['authorization']['token'] opts[:pid_file].should == config['pid_file'] opts[:log_file].should == config['log_file'] opts[:log_time].should == config['logtime'] opts[:debug].should == config['debug'] opts[:trace].should == config['trace'] opts[:max_control_line].should == config['max_control_line'] opts[:max_payload].should == config['max_payload'] opts[:max_pending].should == config['max_pending'] # cluster specific opts[:cluster_port].should == config['cluster']['port'] opts[:cluster_user].should == config['cluster']['authorization']['user'] opts[:cluster_pass].should == config['cluster']['authorization']['password'] opts[:cluster_token].should == config['cluster']['authorization']['token'] opts[:cluster_auth_timeout].should == config['cluster']['authorization']['timeout'] opts[:cluster_routes].should == config['cluster']['routes'] end end ================================================ FILE: spec/server/server_config_spec.rb ================================================ require 'spec_helper' require 'nats/server/server' require "nats/server/sublist" require "nats/server/options" require "nats/server/const" require "nats/server/util" require 'logger' describe "Server - Configuration" do it 'should return default options with no command line arguments' do NATSD::Server.process_options opts = NATSD::Server.options opts.should be_an_instance_of Hash opts.should have_key :port opts.should have_key :addr opts.should have_key :max_control_line opts.should have_key :max_payload opts.should have_key :max_pending opts.should have_key :max_connections opts[:port].should == NATSD::DEFAULT_PORT opts[:addr].should == NATSD::DEFAULT_HOST end it 'should allow an override with command line arguments' do NATSD::Server.process_options('-a localhost -p 5222 --user derek --pass foo'.split) opts = NATSD::Server.options opts[:addr].should == 'localhost' opts[:port].should == 5222 opts[:user].should == 'derek' opts[:pass].should == 'foo' end it 'should properly parse a config file' do config_file = File.dirname(__FILE__) + '/resources/config.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options opts[:config_file].should == config_file opts[:port].should == config['port'] opts[:addr].should == config['net'] opts[:user].should == config['authorization']['user'] opts[:pass].should == config['authorization']['password'] opts[:token].should == config['authorization']['token'] opts[:ssl].should == config['ssl'] opts[:pid_file].should == config['pid_file'] opts[:log_file].should == config['log_file'] opts[:log_time].should == config['logtime'] opts[:debug].should == config['debug'] opts[:trace].should == config['trace'] opts[:max_control_line].should == config['max_control_line'] opts[:max_payload].should == config['max_payload'] opts[:max_pending].should == config['max_pending'] opts[:max_connections].should == config['max_connections'] end it 'should allow pass and password for authorization config' do config_file = File.dirname(__FILE__) + '/resources/auth.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options opts[:user].should == config['authorization']['user'] opts[:pass].should == config['authorization']['pass'] end it 'should allow command line arguments to override config file' do config_file = File.dirname(__FILE__) + '/resources/config.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file} -p 8122 -l /tmp/foo.log".split) opts = NATSD::Server.options opts[:port].should == 8122 opts[:log_file].should == '/tmp/foo.log' end it 'should properly set logtime under server attributes' do config_file = File.dirname(__FILE__) + '/resources/config.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) NATSD::Server.finalize_options NATSD::Server.log_time.should be_truthy end describe "NATSD::Server.finalize_options" do before do NATSD::Server.process_options end context "ssl setting is nothing" do before do NATSD::Server.options[:ssl] = nil end it "shoud properly set @ssl_required to nil" do NATSD::Server.finalize_options NATSD::Server.ssl_required.should be_falsey end end context "ssl setting is false" do before do NATSD::Server.options[:ssl] = false end it "should properly set @ssl_required to false" do NATSD::Server.finalize_options NATSD::Server.ssl_required.should be_falsey end end context "ssl setting is true" do before do NATSD::Server.options[:ssl] = true end it "should properly set @ssl_required to true" do NATSD::Server.finalize_options NATSD::Server.ssl_required.should be_truthy end end end end ================================================ FILE: spec/server/server_exitcode_spec.rb ================================================ require 'spec_helper' require 'fileutils' describe 'Server - exit codes' do before (:all) do config_file = File.dirname(__FILE__) + '/resources/nonexistant.yml' uri = 'nats://localhost:4222' pid_file = '/tmp/test-nats-exit.pid' @s = RubyNatsServerControl.new(uri, pid_file, "-c #{config_file}") end after(:all) do @s.kill_server end it 'should exit with non-zero status code when config file not found' do exit_code = @s.start_server(false) exit_code.should_not == 0 end end ================================================ FILE: spec/server/server_log_spec.rb ================================================ require 'spec_helper' require 'syslog' require 'nats/server/server' require 'nats/server/const' require 'nats/server/options' require 'nats/server/util' describe 'Server - log and pid files' do before(:all) do LOG_SERVER_PID = '/tmp/nats_log_pid.pid' LOG_SERVER = 'nats://localhost:9299' LOG_LOG_FILE = '/tmp/nats_log_test.log' LOG_FLAGS = "-l #{LOG_LOG_FILE}" SYSLOG_IDENT = "nats_syslog_test" LOG_SYSLOG_FLAGS= "#{LOG_FLAGS} -S #{SYSLOG_IDENT}" FileUtils.rm_f(LOG_LOG_FILE) @s = RubyNatsServerControl.new(LOG_SERVER, LOG_SERVER_PID, LOG_FLAGS) @s.start_server end after(:all) do @s.kill_server NATS.server_running?(LOG_SERVER).should be_falsey FileUtils.rm_f(LOG_LOG_FILE) end it 'should create the log file' do File.exists?(LOG_LOG_FILE).should be_truthy end it 'should create the pid file' do File.exists?(LOG_SERVER_PID).should be_truthy end it 'should not leave a daemonized pid file in current directory' do File.exists?("./#{NATSD::APP_NAME}.pid").should be_falsey end it 'should append to the log file after restart' do @s.kill_server @s.start_server File.read(LOG_LOG_FILE).split("\n").size.should == 4 end it 'should not output to the log file when enable syslog option' do @s.kill_server FileUtils.rm_f(LOG_LOG_FILE) @s = RubyNatsServerControl.new(LOG_SERVER, LOG_SERVER_PID, LOG_SYSLOG_FLAGS) @s.start_server File.read(LOG_LOG_FILE).split("\n").size.should == 0 end it 'should use Syslog module methods when enable syslog option' do *log_msg = 'syslog test' Syslog.should_receive(:open).with(SYSLOG_IDENT, Syslog::LOG_PID, Syslog::LOG_USER) Syslog.should_receive(:log).with(Syslog::LOG_NOTICE, '%s', PP::pp(log_msg, '', 120)) Syslog.should_receive(:close) NATSD::Server.process_options(LOG_SYSLOG_FLAGS.split) NATSD::Server.open_syslog log *log_msg NATSD::Server.close_syslog end end ================================================ FILE: spec/server/server_ping_spec.rb ================================================ require 'spec_helper' require 'fileutils' require 'nats/server/server' require 'nats/server/options' require 'nats/server/const' require 'nats/server/util' describe 'Server - ping' do before (:all) do config_file = File.dirname(__FILE__) + '/resources/ping.yml' config = File.open(config_file) { |f| YAML.load(f) } NATSD::Server.process_options("-c #{config_file}".split) @opts = NATSD::Server.options @log_file = config['log_file'] @host = config['net'] @port = config['port'] @uri = "nats://#{@host}:#{@port}" @rs = RubyNatsServerControl.new(@uri, config['pid_file'], "-c #{config_file}") @rs.start_server end after(:all) do @rs.kill_server FileUtils.rm_f(@log_file) end it 'should set default values for ping if not set' do config_file = File.dirname(__FILE__) + '/resources/config.yml' NATSD::Server.process_options("-c #{config_file}".split) opts = NATSD::Server.options opts[:ping_interval].should == 120 opts[:ping_max].should == 2 end it 'should properly parse ping parameters from config file' do @opts[:ping_interval].should == 0.1 @opts[:ping_max].should == 2 end it 'should ping us periodically' do NATS.start(:uri => @uri) do |connection| time_to_wait = @opts[:ping_interval] * @opts[:ping_max] + 0.2 EM.add_timer(time_to_wait) do connection.pings.should >= @opts[:ping_max] NATS.stop end end end it 'should disconnect us when we do not respond' do begin s = TCPSocket.open(@host, @port) time_to_wait = @opts[:ping_interval] * (@opts[:ping_max] + 2) sleep(time_to_wait) buf = s.read(1024) buf.should =~ /PING/ buf.should =~ /-ERR/ buf.should =~ /Unresponsive client detected, connection dropped/ ensure s.close if s end end end ================================================ FILE: spec/server/ssl_spec.rb ================================================ require 'spec_helper' require 'fileutils' describe 'Server - SSL' do TEST_SERVER_SSL = "nats://127.0.0.1:9392" TEST_SERVER_SSL_PID = '/tmp/nats_ssl.pid' TEST_SERVER_NO_SSL = "nats://127.0.0.1:9394" TEST_SERVER_NO_SSL_PID = '/tmp/nats_no_ssl.pid' before (:all) do @s_ssl = RubyNatsServerControl.new(TEST_SERVER_SSL, TEST_SERVER_SSL_PID, "--ssl") @s_ssl.start_server @s_no_ssl = RubyNatsServerControl.new(TEST_SERVER_NO_SSL, TEST_SERVER_NO_SSL_PID) @s_no_ssl.start_server end after (:all) do @s_ssl.kill_server @s_no_ssl.kill_server FileUtils.rm_f TEST_SERVER_SSL_PID FileUtils.rm_f TEST_SERVER_NO_SSL_PID end it 'should fail to connect to an ssl server without TLS/SSL negotiation' do skip 'flapping test' errors = [] with_em_timeout(3) do |future| nc = nil NATS.on_error do |e| errors << e future.resume(nc) end nc = NATS.connect(:uri => TEST_SERVER_SSL) end expect(errors.count > 0).to eql(true) expect(errors.first).to be_a(NATS::Error) end it 'should fail to connect to an no ssl server with TLS/SSL negotiation' do skip 'flapping test' errors = [] with_em_timeout(3) do |future| nc = nil NATS.on_error do |e| errors << e future.resume(nc) end nc = NATS.connect(:uri => TEST_SERVER_NO_SSL, :ssl => true) end expect(errors.count > 0).to eql(true) expect(errors.first).to be_a(NATS::ClientError) end it 'should run TLS/SSL negotiation' do expect do NATS.start(:uri => TEST_SERVER_SSL, :ssl => true) { NATS.stop } end.to_not raise_error end it 'should not run TLS/SSL negotiation' do expect do NATS.start(:uri => TEST_SERVER_NO_SSL) { NATS.stop } end.to_not raise_error end end ================================================ FILE: spec/server/sublist_spec.rb ================================================ require 'spec_helper' require 'nats/server/sublist' describe 'Server - sublist functionality' do before do @sublist = Sublist.new end it 'should be empty on start' do @sublist.count.should == 0 @sublist.match('a').should be_empty @sublist.match('a.b').should be_empty end it 'should have N items after N items inserted' do 5.times { |n| @sublist.insert("a.b.#{n}", 'foo') } @sublist.count.should == 5 end it 'should be safe to remove an non-existant subscription' do @sublist.insert('a.b.c', 'foo') @sublist.remove('a.b.x', 'foo') end it 'should have 0 items after N items inserted and removed' do 5.times { |n| @sublist.insert("a.b.#{n}", 'foo') } @sublist.count.should == 5 5.times { |n| @sublist.remove("a.b.#{n}", 'foo') } @sublist.count.should == 0 end it 'should have N items after N items inserted and others removed' do 5.times { |n| @sublist.insert("a.b.#{n}", 'foo') } @sublist.count.should == 5 @sublist.remove('a.b.c', 'foo') @sublist.count.should == 5 end it 'should match and return proper closure for subjects' do @sublist.insert('a', 'foo') m = @sublist.match('a') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' end it 'should not match after the item has been removed' do @sublist.insert('a', 'foo') @sublist.remove('a', 'foo') @sublist.match('a').should be_empty end it 'should match simple multiple tokens' do @sublist.insert('a.b.c', 'foo') m = @sublist.match('a.b.c') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' m = @sublist.match('a.b.z') m.should be_empty end it 'should match full wildcards on any proper subject' do @sublist.insert('a.b.>', 'foo') m = @sublist.match('a.b.c') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' m = @sublist.match('a.b.z') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' m = @sublist.match('a.b.c.d.e.f') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' end it 'should match positional wildcards on any proper subject' do @sublist.insert('a.*.c', 'foo') m = @sublist.match('a.b.c') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' m = @sublist.match('a.b.z') m.should be_empty m = @sublist.match('a.z.c') m.should_not be_empty m.size.should == 1 m[0].should == 'foo' end it 'should properly match multiple wildcards' do @sublist.insert('*.b.*', '2pwc') m = @sublist.match('a.b.c') m.count.should == 1 m.should == ['2pwc'] @sublist.insert('a.>', 'fwc') m = @sublist.match('a.z.c') m.count.should == 1 m.should == ['fwc'] m = @sublist.match('a.b.c.d') m.count.should == 1 m.should == ['fwc'] end it 'should properly mix and match simple, fwc, and pwc wildcards' do @sublist.insert('a.b.c', 'simple') @sublist.insert('a.b.>', 'fwc') @sublist.insert('a.*.c', 'pwc-middle') @sublist.insert('*.b.c', 'pwc-first') @sublist.insert('a.b.*', 'pwc-last') m = @sublist.match('a.b.c').sort m.count.should == 5 m.should == ['fwc', 'pwc-first', 'pwc-last', 'pwc-middle', 'simple'] m = @sublist.match('a.b.c.d.e.f') m.count.should == 1 m.should == ['fwc'] m = @sublist.match('z.b.c') m.count.should == 1 m.should == ['pwc-first'] m = @sublist.match('a.z.c') m.count.should == 1 m.should == ['pwc-middle'] m = @sublist.match('a.b.z').sort m.count.should == 2 m.should == ['fwc', 'pwc-last'] end it 'should have N node_count after N items inserted' do 5.times { |n| @sublist.insert("a.b.#{n}", 'foo') } nc = @sublist.send :node_count nc.should == 7 end it 'should have 0 node_count after N items inserted and removed' do 5.times { |n| @sublist.insert("#{n}", 'foo') } 5.times { |n| @sublist.remove("#{n}", 'foo') } nc = @sublist.send :node_count nc.should == 0 end it 'should have 0 node_count after N items with 1 token prefix inserted and removed' do 5.times { |n| @sublist.insert("INBOX.#{n}", 'foo') } 5.times { |n| @sublist.remove("INBOX.#{n}", 'foo') } nc = @sublist.send :node_count nc.should == 0 end it 'should have 0 node_count after N items with 3 prefix and wildcards inserted and removed' do 5.times { |n| @sublist.insert("a.b.*.#{n}", 'foo') } @sublist.insert('a.b.>', 'foo') 5.times { |n| @sublist.remove("a.b.*.#{n}", 'foo') } @sublist.remove('a.b.>', 'foo') nc = @sublist.send :node_count nc.should == 0 end it 'should have 0 node_count after N items with large middle token range inserted and removed' do 5.times { |n| @sublist.insert("a.#{n}.c", 'foo') } 5.times { |n| @sublist.remove("a.#{n}.c", 'foo') } nc = @sublist.send :node_count nc.should == 0 end end ================================================ FILE: spec/spec_helper.rb ================================================ $:.unshift('./lib') require 'net/http' require 'nats/client' require 'tempfile' def timeout_nats_on_failure(to=0.25) EM.add_timer(to) do EM.stop end end def timeout_em_on_failure(to=0.25) EM.add_timer(to) do EM.stop end end def with_em_timeout(to=1) EM.run do t = EM.add_timer(to) do NATS.stop EM.stop if EM.reactor_running? end fib = Fiber.new do |nc| nc.close if nc EM.cancel_timer(t) EM.stop if EM.reactor_running? end yield fib end end def wait_on_connections(conns) return unless conns expected, ready = conns.size, 0 conns.each do |c| c.flush do ready += 1 yield if ready >= expected end end end def flush_routes(conns, &blk) wait_on_routes_connected(conns, &blk) end def wait_on_routes_connected(conns) return unless conns && conns.size > 1 sub = NATS.create_inbox ready = Array.new(conns.size, false) yield_needed = true conns.each_with_index do |c, i| c.subscribe(sub) do |msg| ready[i] = true done, yn = ready.all?, yield_needed yield_needed = false if yn && done yield if yn && done end end conn = conns.first timer = EM.add_periodic_timer(0.1) do conn.publish(sub) timer.cancel if ready.all? end end class NatsServerControl attr_reader :was_running alias :was_running? :was_running class << self def init_with_config(config_file) config = File.open(config_file) { |f| YAML.load(f) } if auth = config['authorization'] uri = "nats://#{auth['user']}:#{auth['password']}@#{config['net']}:#{config['port']}" else uri = "nats://#{config['net']}:#{config['port']}" end NatsServerControl.new(uri, config['pid_file'], "-c #{config_file}") end def init_with_config_from_string(config_string, config={}) puts config_string if ENV["DEBUG_NATS_TEST"] == "true" config_file = Tempfile.new(['nats-cluster-tests', '.conf']) File.open(config_file.path, 'w') do |f| f.puts(config_string) end if auth = config['authorization'] uri = "nats://#{auth['user']}:#{auth['password']}@#{config['host']}:#{config['port']}" else uri = "nats://#{config['host']}:#{config['port']}" end NatsServerControl.new(uri, config['pid_file'], "-c #{config_file.path}", config_file) end end attr_reader :uri def initialize(uri='nats://127.0.0.1:4222', pid_file='/tmp/test-nats.pid', flags=nil, config_file=nil) @uri = uri.is_a?(URI) ? uri : URI.parse(uri) @pid_file = pid_file @flags = flags @config_file = config_file end def server_pid @pid ||= File.read(@pid_file).chomp.to_i end def server_mem_mb server_status = %x[ps axo pid=,rss= | grep #{server_pid}] parts = server_status.lstrip.split(/\s+/) rss = (parts[1].to_i)/1024 end def start_server(wait_for_server=true, monitoring: false) if NATS.server_running? @uri @was_running = true return 0 end @pid = nil args = "-p #{@uri.port} -P #{@pid_file}" args += " -m 8222" if monitoring args += " --user #{@uri.user}" if @uri.user args += " --pass #{@uri.password}" if @uri.password args += " #{@flags}" if @flags if ENV["DEBUG_NATS_TEST"] == "true" system("nats-server #{args} -DV &") else system("nats-server #{args} 2> /dev/null &") end exitstatus = $?.exitstatus NATS.wait_for_server(@uri, 10) if wait_for_server # jruby can be slow on startup... exitstatus end def kill_server if File.exists? @pid_file %x[kill -9 #{server_pid} 2> /dev/null] %x[rm #{@pid_file} 2> /dev/null] sleep(0.1) @pid = nil end end end # For running tests against the Ruby NATS server :) class RubyNatsServerControl < NatsServerControl def start_server(wait_for_server=true) if NATS.server_running? @uri @was_running = true return 0 end @pid = nil # daemonize really doesn't work on jruby, so should run servers manually to test on jruby args = "-p #{@uri.port} -P #{@pid_file}" args += " --user #{@uri.user}" if @uri.user args += " --pass #{@uri.password}" if @uri.password args += " #{@flags}" if @flags args += ' -d' %x[bundle exec ruby ./bin/nats-server #{args} 2> /dev/null] exitstatus = $?.exitstatus NATS.wait_for_server(@uri, 10) if wait_for_server #jruby can be slow on startup exitstatus end end module EchoServer HOST = "127.0.0.1".freeze PORT = "9999".freeze URI = "nats://#{HOST}:#{PORT}".freeze def post_init send_data('INFO {"server_id":"WxE17fQB24XOvaWlhECrhg","max_payload":1048576,"client_id":1}'+"\r\n") end def receive_data(data) send_data(data) end class << self def start(&blk) EM.run { EventMachine::start_server(HOST, PORT, self) blk.call } end def stop EM.stop_event_loop end end end module SilentServer HOST = "127.0.0.1".freeze PORT = "9998".freeze URI = "nats://#{HOST}:#{PORT}".freeze def post_init send_data('INFO {"server_id":"WxE17fQB24XOvaWlhECrhg","max_payload":1048576,"client_id":1}'+"\r\n") end # Does not send anything back def receive_data(data) end class << self def start(&blk) EM.run { EventMachine::start_server(HOST, PORT, self) blk.call } end def stop EM.stop_event_loop end end end module OldInfoServer HOST = "127.0.0.1".freeze PORT = "9997".freeze URI = "nats://#{HOST}:#{PORT}".freeze def post_init send_data('INFO {"server_id":"WxE17fQB24XOvaWlhECrhg","version":"1.3.0","host":"0.0.0.0","port":4222,"max_payload":1048576,"client_id":1}'+"\r\n") end def receive_data(data) end class << self def start(&blk) EM.run { EventMachine::start_server(HOST, PORT, self) blk.call } end def stop EM.stop_event_loop end end end module OldProtocolInfoServer HOST = "127.0.0.1".freeze PORT = "9996".freeze URI = "nats://#{HOST}:#{PORT}".freeze def post_init send_data('INFO {"server_id":"WxE17fQB24XOvaWlhECrhg","version":"1.3.0","host":"0.0.0.0","port":4222,"max_payload":1048576,"client_id":1,"proto": 0}'+"\r\n") end def receive_data(data) end class << self def start(&blk) EM.run { EventMachine::start_server(HOST, PORT, self) blk.call } end def stop EM.stop_event_loop end end end