├── .env-ci ├── .env-dev ├── .gitignore ├── .gitmodules ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CHANGES.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── celluloid-zmq.gemspec ├── examples └── publish_subscribe.rb ├── lib └── celluloid │ ├── zmq.rb │ └── zmq │ ├── current.rb │ ├── deprecate.rb │ ├── mailbox.rb │ ├── reactor.rb │ ├── socket.rb │ ├── socket │ ├── readable.rb │ ├── types.rb │ └── writable.rb │ ├── version.rb │ └── waker.rb ├── log └── .gitignore ├── logo.png ├── spec ├── celluloid │ ├── zmq │ │ ├── actor_spec.rb │ │ ├── mailbox_spec.rb │ │ └── socket_spec.rb │ └── zmq_spec.rb └── spec_helper.rb └── tasks ├── rspec.rake └── rubocop.rake /.env-ci: -------------------------------------------------------------------------------- 1 | CELLULOID_SPECS_LOG_STRATEGY=stderr 2 | CELLULOID_SPECS_LOG_LEVEL=3 3 | CELLULOID_SPECS_LOG_FILE=log/ci.log 4 | CELLULOID_SPECS_LOG_SYNC=false 5 | -------------------------------------------------------------------------------- /.env-dev: -------------------------------------------------------------------------------- 1 | CELLULOID_SPECS_LOG_STRATEGY=single 2 | CELLULOID_SPECS_LOG_FILE=log/test.log 3 | CELLULOID_SPECS_LOG_LEVEL=0 4 | CELLULOID_SPECS_LOG_SYNC=true 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "culture"] 2 | path = culture 3 | url = http://github.com/celluloid/culture.git 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --tty 3 | --format documentation 4 | --backtrace 5 | --order random 6 | --require spec_helper 7 | --warnings 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - culture/rubocop/rubocop.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - PATH="/usr/lib/ccache:$PATH" # enable ccache 3 | - export LD_LIBRARY_PATH=$HOME/lib # custom libs (for execution) 4 | - export PKG_CONFIG_PATH=$HOME/lib/pkgconfig # custom libs (for linking) 5 | - export BUNDLE_PATH=$HOME/.bundle # bundle caching 6 | - ( mkdir -p vendor && cd vendor && git clone --depth 1 https://github.com/paddor/czmq-ffi-gen && cd czmq-ffi-gen && ci-scripts/install-libzmq && ci-scripts/install-libczmq; ) 7 | sudo: false 8 | cache: 9 | directories: 10 | - $HOME/.ccache 11 | - $HOME/.bundle 12 | script: bundle exec rake ci 13 | language: ruby 14 | install: bundle install --without=development 15 | rvm: 16 | - rbx 17 | - 2.2.3 18 | - jruby-9.1.2.0 19 | - 2.1.8 20 | - 2.3.1 21 | - ruby-head 22 | - jruby-head 23 | matrix: 24 | fast_finish: true 25 | allow_failures: 26 | - rvm: jruby-9.1.2.0 27 | - rvm: 2.1.8 28 | - rvm: ruby-head 29 | - rvm: jruby-head 30 | - env: CELLULOID_BACKPORTED=true 31 | - env: CELLULOID_BACKPORTED=false CELLULOID_LEAKTEST=true 32 | - env: CELLULOID_BACKPORTED=false CELLULOID_TASK_CLASS=Threaded 33 | - env: CELLULOID_BACKPORTED=true CELLULOID_TASK_CLASS=Threaded 34 | env: 35 | global: 36 | - NUMBER_OF_PROCESSORS=4 CELLULOID_CONFIG_FILE=.env-ci 37 | # recognized by czmq-ffi-gen's ci-scripts 38 | - CZMQ_VERSION=HEAD ZMQ_VERSION=HEAD 39 | matrix: 40 | - CELLULOID_BACKPORTED=true 41 | - CELLULOID_BACKPORTED=false 42 | - CELLULOID_BACKPORTED=false CELLULOID_LEAKTEST=true 43 | - CELLULOID_BACKPORTED=false CELLULOID_TASK_CLASS=Threaded 44 | - CELLULOID_BACKPORTED=true CELLULOID_TASK_CLASS=Threaded 45 | notifications: 46 | irc: irc.freenode.org#celluloid 47 | slack: 48 | secure: uJ8uoiNgiEDoRewbH6gj9mphUGVDtjXeEy8++vSTQkLqIkkoZ3M+mr0yldL2/ECaG8wHLH2035DHM4d54GyeVEU/8UG80UVAnYTctlzzjn1rfXPfCIsZDXYMUjXe3wvOouN+b4hjiyXe7ZsssdRoeKw6rHIU8/tUHgC3IfZel7s= 49 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.17.2 (2015-09-30) 2 | ----- 3 | * Revamped test suite, using shared RSpec configuration layer provided by Celluloid itself. 4 | * Updated gem dependencies provided by Celluloid::Sync... extraneous gems removed, or marked as development dependencies. 5 | 6 | 0.17.0 (2015-08-15) 7 | ----- 8 | * Adapted to be compliant with version 0.17.0 of Celluloid. 9 | * Added `write_to` for use with `Router` sockets. 10 | * Added more direct set/get of socket identity. 11 | 12 | 0.16.1 (2015-04-26) 13 | ----- 14 | * Support for XPUB sockets 15 | * Support for reading multipart messages 16 | * Spec cleanup 17 | 18 | 0.16.0 (2014-09-04) 19 | ----- 20 | * Support for setting socket options 21 | * More specs 22 | 23 | 0.15.0 (2013-09-04) 24 | ----- 25 | * Tracking release for Celluloid 0.15 26 | 27 | 0.14.0 (2013-05-07) 28 | ----- 29 | * Add pubsub example 30 | * Add identity support to Sockets 31 | * Depend on EventedMailbox from core instead of celluloid-io 32 | * Remove overhead for IO waiting by calling directly to the reactor 33 | 34 | 0.13.0 35 | ----- 36 | * Feature: Support for DealerSocket and RouterSocket 37 | * Support for the #more_parts? method on sockets 38 | * Celluloid 0.13 compatibility fixes 39 | 40 | 0.12.0 41 | ----- 42 | * Tracking release for Celluloid 0.12.0 43 | 44 | 0.10.0 45 | ----- 46 | * Factor celluloid-zmq into its own gem 47 | * #linger= support 48 | 49 | 0.9.0 50 | ----- 51 | * New 0MQ APIs which wrap ffi-rzmq's 52 | * Terminate the 0MQ context at shutdown 53 | * Use Celluloid::IO 0.9.0's reactor injection support so we no longer have to 54 | subclass Celluloid::IO::Mailbox 55 | 56 | 0.8.0 57 | ----- 58 | * Update to match internals of celluloid-io 59 | 60 | 0.7.0 61 | ----- 62 | * Use celluloid-io gem 63 | * Match versions with Celluloid 64 | 65 | 0.0.4 66 | ----- 67 | * Fix bugs in 0MQ polling (timeouts were being processed 1000x too fast) 68 | 69 | 0.0.3 70 | ----- 71 | * Fix botched dependencies (celluloid-zmq does not depend on redis) 72 | 73 | 0.0.2 74 | ----- 75 | * Pure blocking 0MQ reactor using a ZMQ::PAIR socket as the waker 76 | 77 | 0.0.1 78 | ----- 79 | * Initial release 80 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | require File.expand_path("../culture/sync", __FILE__) 2 | Celluloid::Sync::Gemfile[self] 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Tony Arcieri 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Celluloid::ZMQ](https://github.com/celluloid/celluloid-zmq/raw/master/logo.png) 2 | ================= 3 | [![Gem Version](https://badge.fury.io/rb/celluloid-zmq.png)](http://rubygems.org/gems/celluloid-zmq) 4 | [![Build Status](https://secure.travis-ci.org/celluloid/celluloid-zmq.png?branch=master)](http://travis-ci.org/celluloid/celluloid-zmq) 5 | [![Code Climate](https://codeclimate.com/github/celluloid/celluloid-zmq.png)](https://codeclimate.com/github/celluloid/celluloid-zmq) 6 | [![Coverage Status](https://coveralls.io/repos/celluloid/celluloid-zmq/badge.png?branch=master)](https://coveralls.io/r/celluloid/celluloid-zmq) 7 | 8 | `Celluloid::ZMQ` provides Celluloid actors that can interact with [0MQ sockets][0mq]. 9 | Underneath, it's built on the [CZTop][cztop] library. `Celluloid::ZMQ` was 10 | primarily created for the purpose of writing [DCell][dcell], distributed Celluloid 11 | over 0MQ, so before you go building your own distributed Celluloid systems with 12 | `Celluloid::ZMQ`, be sure to give DCell a look and decide if it fits your purposes. 13 | 14 | [0mq]: http://www.zeromq.org/ 15 | [cztop]: https://github.com/paddor/cztop 16 | [dcell]: https://github.com/celluloid/dcell 17 | 18 | It provides different `Celluloid::ZMQ::Socket` classes which can be initialized 19 | then sent `bind` or `connect`. Once bound or connected, the socket can 20 | `read` or `send` depending on whether it's readable or writable. 21 | 22 | ## Supported Platforms 23 | 24 | You will need the ZeroMQ library and the CZMQ library installed as it's 25 | accessed via FFI. See [CZTop][cztop] for installation instructions. 26 | 27 | Supported Rubies are MRI >= 2.2, JRuby >= 9.0.4.0, and Rubinius >= 3.7. 28 | 29 | ## 0MQ Socket Types 30 | 31 | The following 0MQ socket types are supported (see [types.rb][types] for more info) 32 | 33 | [types]: https://github.com/celluloid/celluloid-zmq/blob/master/lib/celluloid/zmq/socket/types.rb 34 | 35 | * Req / Rep 36 | * Push / Pull 37 | * Pub / Sub 38 | * Dealer / Router 39 | 40 | ## Usage 41 | 42 | ```ruby 43 | require 'celluloid/zmq' 44 | 45 | class Server 46 | include Celluloid::ZMQ 47 | 48 | def initialize(address) 49 | @socket = Socket::Pull.new 50 | 51 | begin 52 | @socket.bind(address) 53 | rescue IOError 54 | @socket.close 55 | raise 56 | end 57 | end 58 | 59 | def run 60 | loop { async.handle_message @socket.read } 61 | end 62 | 63 | def handle_message(message) 64 | puts "got message: #{message}" 65 | end 66 | end 67 | 68 | class Client 69 | include Celluloid::ZMQ 70 | 71 | def initialize(address) 72 | @socket = Socket::Push.new 73 | 74 | begin 75 | @socket.connect(address) 76 | rescue IOError 77 | @socket.close 78 | raise 79 | end 80 | end 81 | 82 | def write(message) 83 | @socket << message 84 | nil 85 | end 86 | end 87 | 88 | addr = 'tcp://127.0.0.1:3435' 89 | 90 | server = Server.new(addr) 91 | client = Client.new(addr) 92 | 93 | server.async.run 94 | client.write('hi') 95 | 96 | sleep 97 | ``` 98 | 99 | Copyright 100 | --------- 101 | 102 | Copyright (c) 2014-2015 Tony Arcieri, Donovan Keme. 103 | 104 | Distributed under the MIT License. See [LICENSE.txt](https://github.com/celluloid/celluloid/blob/master/LICENSE.txt) for further details. 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "rspec/core/rake_task" 4 | 5 | RSpec::Core::RakeTask.new 6 | 7 | Dir["tasks/**/*.rake"].each { |task| load task } 8 | 9 | default_tasks = ["spec"] 10 | default_tasks << "rubocop" unless ENV["CI"] 11 | 12 | task default: default_tasks 13 | task ci: %w(spec) 14 | -------------------------------------------------------------------------------- /celluloid-zmq.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path("../culture/sync", __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Tony Arcieri"] 6 | gem.email = ["tony.arcieri@gmail.com"] 7 | gem.description = "Celluloid bindings to the CZMQ library" 8 | gem.summary = "Celluloid::ZMQ provides concurrent Celluloid actors that can listen for 0MQ events" 9 | gem.homepage = "http://github.com/celluloid/celluloid-zmq" 10 | gem.license = "MIT" 11 | 12 | gem.name = "celluloid-zmq" 13 | gem.version = Celluloid::ZMQ::VERSION 14 | 15 | Celluloid::Sync::Gemspec[gem] 16 | gem.add_dependency "cztop" 17 | 18 | # Files 19 | ignores = File.read(".gitignore").split(/\r?\n/).reject { |f| f =~ /^(#.+|\s*)$/ }.map { |f| Dir[f] }.flatten 20 | gem.files = (Dir["**/*", ".gitignore"] - ignores).reject { |f| !File.file?(f) } 21 | gem.test_files = (Dir["spec/**/*", ".gitignore"] - ignores).reject { |f| !File.file?(f) } 22 | # gem.executables = Dir['bin/*'].map { |f| File.basename(f) } 23 | gem.require_paths = ["lib"] 24 | end 25 | -------------------------------------------------------------------------------- /examples/publish_subscribe.rb: -------------------------------------------------------------------------------- 1 | require "celluloid/zmq/current" 2 | 3 | Celluloid::ZMQ.init 4 | 5 | class PublishSubscribe 6 | include Celluloid::ZMQ 7 | 8 | def run 9 | link = "tcp://127.0.0.1:5555" 10 | 11 | s1 = Socket::Pub.new 12 | s2 = Socket::Sub.new 13 | s3 = Socket::Sub.new 14 | s4 = Socket::Sub.new 15 | s5 = Socket::Sub.new 16 | 17 | s1.linger = 100 18 | s2.subscribe("") # receive all 19 | s3.subscribe("animals") # receive any starting with this string 20 | s4.subscribe("animals.dog") 21 | s5.subscribe("animals.cat") 22 | 23 | s1.bind(link) 24 | s2.connect(link) 25 | s3.connect(link) 26 | s4.connect(link) 27 | s5.connect(link) 28 | 29 | sleep 1 30 | 31 | topic = "animals.dog" 32 | payload = "Animal crackers!" 33 | 34 | s1.identity = "publisher-A" 35 | puts "sending" 36 | # use the new multi-part messaging support to 37 | # automatically separate the topic from the body 38 | s1.write(topic, payload, s1.identity) 39 | 40 | topic = "" 41 | s2.read(topic) 42 | 43 | body = "" 44 | s2.read(body) if s2.more_parts? 45 | 46 | identity = "" 47 | s2.read(identity) if s2.more_parts? 48 | puts "s2 received topic [#{topic}], body [#{body}], identity [#{identity}]" 49 | 50 | topic = "" 51 | s3.read(topic) 52 | 53 | body = "" 54 | s3.read(body) if s3.more_parts? 55 | puts "s3 received topic [#{topic}], body [#{body}]" 56 | 57 | topic = "" 58 | s4.read(topic) 59 | 60 | body = "" 61 | s4.read(body) if s4.more_parts? 62 | puts "s4 received topic [#{topic}], body [#{body}]" 63 | 64 | s5_string = "" 65 | s5.read(s5_string) 66 | 67 | # we will never get here 68 | end 69 | end 70 | 71 | PublishSubscribe.new.run 72 | -------------------------------------------------------------------------------- /lib/celluloid/zmq.rb: -------------------------------------------------------------------------------- 1 | require 'cztop' 2 | 3 | $CELLULOID_ZMQ_BACKPORTED = (ENV["CELLULOID_ZMQ_BACKPORTED"] != "false") unless defined?($CELLULOID_ZMQ_BACKPORTED) 4 | 5 | require ($CELLULOID_ZMQ_BACKPORTED) ? "celluloid" : "celluloid/current" 6 | 7 | require "celluloid/zmq/mailbox" 8 | require "celluloid/zmq/reactor" 9 | require "celluloid/zmq/socket" 10 | require "celluloid/zmq/version" 11 | require "celluloid/zmq/waker" 12 | 13 | require "celluloid/zmq/socket/readable" 14 | require "celluloid/zmq/socket/writable" 15 | require "celluloid/zmq/socket/types" 16 | 17 | module Celluloid 18 | # Actors which run alongside 0MQ sockets 19 | module ZMQ 20 | class UninitializedError < Celluloid::Error; end 21 | 22 | class << self 23 | attr_writer :context 24 | 25 | # Included hook to pull in Celluloid 26 | def included(klass) 27 | klass.send :include, ::Celluloid 28 | klass.mailbox_class Celluloid::ZMQ::Mailbox 29 | end 30 | 31 | # @deprecated 32 | def init(*) 33 | Celluloid::Internals::Logger.deprecate("Calling .init isn't needed anymore") 34 | nil 35 | end 36 | 37 | # @deprecated 38 | def context 39 | Celluloid::Internals::Logger.deprecate("Accessing ZMQ's context is deprecated") 40 | nil 41 | end 42 | 43 | # @deprecated 44 | def terminate 45 | Celluloid::Internals::Logger.deprecate("Calling .terminate isn't needed anymore") 46 | nil 47 | end 48 | end 49 | 50 | # Is this a Celluloid::ZMQ evented actor? 51 | def self.evented? 52 | actor = Thread.current[:celluloid_actor] 53 | actor.mailbox.is_a?(Celluloid::ZMQ::Mailbox) 54 | end 55 | 56 | def wait_readable(socket) 57 | if ZMQ.evented? 58 | mailbox = Thread.current[:celluloid_mailbox] 59 | mailbox.reactor.wait_readable(socket) 60 | else 61 | fail ArgumentError, "unable to wait for ZMQ sockets outside the event loop" 62 | end 63 | nil 64 | end 65 | module_function :wait_readable 66 | 67 | def wait_writable(socket) 68 | if ZMQ.evented? 69 | mailbox = Thread.current[:celluloid_mailbox] 70 | mailbox.reactor.wait_writable(socket) 71 | else 72 | fail ArgumentError, "unable to wait for ZMQ sockets outside the event loop" 73 | end 74 | nil 75 | end 76 | module_function :wait_writable 77 | 78 | # @deprecated 79 | def result_ok?(_result) 80 | Celluloid::Internals::Logger.deprecate("Checking results of ZMQ operations isn't needed anymore") 81 | true 82 | end 83 | module_function :result_ok? 84 | end 85 | end 86 | 87 | require "celluloid/zmq/deprecate" unless $CELLULOID_BACKPORTED == false || $CELLULOID_ZMQ_BACKPORTED == false 88 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/current.rb: -------------------------------------------------------------------------------- 1 | $CELLULOID_ZMQ_BACKPORTED = false 2 | require "celluloid/zmq" 3 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/deprecate.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | ReadableSocket = Socket::Readable 4 | WritableSocket = Socket::Writable 5 | RepSocket = Socket::Rep 6 | ReqSocket = Socket::Req 7 | DealerSocket = Socket::Dealer 8 | RouterSocket = Socket::Router 9 | PushSocket = Socket::Push 10 | PullSocket = Socket::Pull 11 | PubSocket = Socket::Pub 12 | XPubSocket = Socket::XPub 13 | SubSocket = Socket::Sub 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/mailbox.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | # Replacement mailbox for Celluloid::ZMQ actors 4 | class Mailbox < Celluloid::Mailbox::Evented 5 | def initialize 6 | super(Reactor) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/reactor.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | # React to incoming 0MQ and Celluloid events. This is kinda sorta supposed 4 | # to resemble the Reactor design pattern. 5 | class Reactor 6 | extend Forwardable 7 | def_delegator :@waker, :signal, :wakeup 8 | def_delegator :@waker, :cleanup, :shutdown 9 | 10 | def initialize 11 | @waker = Waker.new 12 | @poller = ::CZTop::Poller::Aggregated.new 13 | @readers = {} 14 | @writers = {} 15 | 16 | @poller.add_reader(@waker.socket) 17 | end 18 | 19 | # Wait for the given ZMQ socket to become readable 20 | def wait_readable(socket) 21 | monitor_zmq socket, @readers, :read 22 | end 23 | 24 | # Wait for the given ZMQ socket to become writable 25 | def wait_writable(socket) 26 | monitor_zmq socket, @writers, :write 27 | end 28 | 29 | # Monitor the given ZMQ socket with the given options 30 | def monitor_zmq(socket, set, type) 31 | if set.key? socket 32 | fail ArgumentError, "another method is already waiting on #{socket.inspect}" 33 | else 34 | set[socket] = Task.current 35 | end 36 | 37 | case type 38 | when :read 39 | @poller.add_reader(socket) 40 | when :write 41 | @poller.add_writer(socket) 42 | else 43 | raise ArgumentError, "wrong type: #{type.inspect}" 44 | end 45 | 46 | Task.suspend :zmqwait 47 | socket 48 | end 49 | 50 | # Run the reactor, waiting for events, and calling the given block if 51 | # the reactor is awoken by the waker 52 | def run_once(timeout = nil) 53 | if timeout 54 | timeout *= 1000 # Poller uses millisecond increments 55 | else 56 | timeout = 0 # blocking 57 | end 58 | 59 | begin 60 | @poller.wait(timeout) 61 | rescue 62 | raise IOError, "ZMQ poll error: #{$!.message}" 63 | end 64 | 65 | @poller.readables.each do |sock| 66 | if sock == @waker.socket 67 | @waker.wait 68 | else 69 | task = @readers.delete sock 70 | @poller.remove_reader(sock) 71 | 72 | if task 73 | task.resume 74 | else 75 | Celluloid::Logger.debug "ZMQ error: got read event without associated reader" 76 | end 77 | end 78 | end 79 | 80 | @poller.writables.each do |sock| 81 | task = @writers.delete sock 82 | @poller.remove_writer(sock) 83 | 84 | if task 85 | task.resume 86 | else 87 | Celluloid::Logger.debug "ZMQ error: got write event without associated writer" 88 | end 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/socket.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | class Socket 4 | 5 | # Create a new socket 6 | def initialize(type) 7 | type = type.is_a?(Integer) ? type : type.to_s.upcase.to_sym 8 | @socket = CZTop::Socket.new_by_type(type) 9 | @linger = 0 10 | end 11 | attr_reader :linger 12 | 13 | # Connect to the given 0MQ address 14 | # Address should be in the form: tcp://1.2.3.4:5678/ 15 | def connect(addr) 16 | @socket.connect addr 17 | true 18 | rescue 19 | raise IOError, "error connecting to #{addr}: #{$!.message}" 20 | end 21 | 22 | def linger=(value) 23 | value ||= -1 24 | @socket.options.linger = value 25 | @linger = value 26 | rescue 27 | raise IOError, "couldn't set linger: #{$!.message}" 28 | end 29 | 30 | def identity=(value) 31 | @socket.options.identity = "#{value}" 32 | rescue 33 | raise IOError, "couldn't set identity: #{$!.message}" 34 | end 35 | 36 | def identity 37 | @socket.options.identity 38 | end 39 | 40 | def set(option, value, _length = nil) 41 | @socket.options[option] = value 42 | rescue 43 | raise IOError, "couldn't set value for option #{option}: #{$!.message}" 44 | end 45 | 46 | def get(option) 47 | @socket.options[option] 48 | rescue 49 | raise IOError, "couldn't get value for option #{option}: #{$!.message}" 50 | end 51 | 52 | # Bind to the given 0MQ address 53 | # Address should be in the form: tcp://1.2.3.4:5678/ 54 | def bind(addr) 55 | @socket.bind(addr) 56 | rescue 57 | raise IOError, "couldn't bind to #{addr}: #{$!.message}" 58 | end 59 | 60 | # Close the socket 61 | def close 62 | @socket.close 63 | end 64 | end 65 | end 66 | end 67 | 68 | unless defined?(::ZMQ) 69 | # Make legacy code like this work: 70 | # 71 | # zmq_socket.set(::ZMQ::IDENTITY, "foo") 72 | # zmq_socket.get(::ZMQ::IDENTITY) 73 | # 74 | # This assumes that the user didn't require 'ffi-rzmq' themselves, but had 75 | # it done by celluloid-zmq. 76 | module ZMQ 77 | def self.const_missing(name) 78 | Celluloid::Internals::Logger.deprecate("Using ZMQ::#{name} as an option name is deprecated. Please report if you need this, so it can be added to Celluloid::ZMQ::Socket.") 79 | return name 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/socket/readable.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | class Socket 4 | # Readable 0MQ sockets have a read method 5 | module Readable 6 | 7 | # always set LINGER on readable sockets 8 | def bind(addr) 9 | self.linger = @linger 10 | super(addr) 11 | end 12 | 13 | def connect(addr) 14 | self.linger = @linger 15 | super(addr) 16 | end 17 | 18 | # Read a message from the socket 19 | def read(buffer = "") 20 | ZMQ.wait_readable(@socket) if ZMQ.evented? 21 | 22 | frame = CZTop::Frame.receive_from(@socket) 23 | buffer << frame.to_s 24 | 25 | @more_parts = frame.more? 26 | buffer 27 | rescue 28 | raise IOError, "error receiving ZMQ string: #{$!.message}" 29 | end 30 | 31 | # Multiparts message ? 32 | def more_parts? 33 | @more_parts 34 | end 35 | 36 | # Reads a multipart message, stores it into the given buffer and returns 37 | # the buffer. 38 | def read_multipart(buffer = []) 39 | ZMQ.wait_readable(@socket) if ZMQ.evented? 40 | 41 | CZTop::Message.receive_from(@socket).to_a.each do |part| 42 | buffer << part 43 | end 44 | 45 | @more_parts = false # we've read all parts 46 | buffer 47 | rescue 48 | raise IOError, "error receiving ZMQ string: #{$!.message}" 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/socket/types.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | class Socket 4 | # ReqSockets are the counterpart of RepSockets (REQ/REP) 5 | class Req < Socket 6 | include Readable 7 | include Writable 8 | 9 | def initialize 10 | super :REQ 11 | end 12 | end 13 | 14 | # RepSockets are the counterpart of ReqSockets (REQ/REP) 15 | class Rep < Socket 16 | include Readable 17 | include Writable 18 | 19 | def initialize 20 | super :REP 21 | end 22 | end 23 | 24 | # DealerSockets are like ReqSockets but more flexible 25 | class Dealer < Socket 26 | include Readable 27 | include Writable 28 | 29 | def initialize 30 | super :DEALER 31 | end 32 | end 33 | 34 | # RouterSockets are like RepSockets but more flexible 35 | class Router < Socket 36 | include Readable 37 | include Writable 38 | 39 | def initialize 40 | super :ROUTER 41 | end 42 | end 43 | 44 | # PushSockets are the counterpart of PullSockets (PUSH/PULL) 45 | class Push < Socket 46 | include Writable 47 | 48 | def initialize 49 | super :PUSH 50 | end 51 | end 52 | 53 | # PullSockets are the counterpart of PushSockets (PUSH/PULL) 54 | class Pull < Socket 55 | include Readable 56 | 57 | def initialize 58 | super :PULL 59 | end 60 | end 61 | 62 | # PubSockets are the counterpart of SubSockets (PUB/SUB) 63 | class Pub < Socket 64 | include Writable 65 | 66 | def initialize 67 | super :PUB 68 | end 69 | end 70 | 71 | # XPubSockets are just like PubSockets but reading from them gives you the 72 | # subscription/unsubscription channels as they're joined/left. 73 | class XPub < Socket 74 | include Writable 75 | include Readable 76 | 77 | def initialize 78 | super :XPUB 79 | end 80 | end 81 | 82 | # SubSockets are the counterpart of PubSockets (PUB/SUB) 83 | class Sub < Socket 84 | include Readable 85 | 86 | def initialize 87 | super :SUB 88 | end 89 | 90 | def subscribe(topic) 91 | @socket.subscribe(topic) 92 | rescue 93 | raise IOError, "couldn't set subscribe: #{$!.message}" 94 | end 95 | 96 | def unsubscribe(topic) 97 | @socket.unsubscribe(topic) 98 | rescue 99 | raise IOError, "couldn't set unsubscribe: #{$!.message}" 100 | end 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/socket/writable.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | class Socket 4 | # Writable 0MQ sockets have a send method 5 | module Writable 6 | 7 | # Send a message to the socket 8 | def write(*messages) 9 | @socket << messages.flatten 10 | messages 11 | rescue 12 | raise IOError, "error sending 0MQ message: #{$!.message}" 13 | end 14 | alias_method :<<, :write 15 | 16 | # @deprecated 17 | alias_method :send, :write 18 | 19 | def write_to(address, message) 20 | @socket.send_to(address, message) 21 | message 22 | rescue 23 | raise IOError, 24 | "error sending message to #{address.inspect}: #{$!.message}" 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/version.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | VERSION = "0.17.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/celluloid/zmq/waker.rb: -------------------------------------------------------------------------------- 1 | module Celluloid 2 | module ZMQ 3 | # You can't wake the dead 4 | DeadWakerError = Class.new IOError 5 | 6 | # Wakes up sleepy threads so that they can check their mailbox 7 | # Works like a ConditionVariable, except it's implemented as a ZMQ socket 8 | # so that it can be multiplexed alongside other ZMQ sockets 9 | class Waker 10 | def initialize 11 | @sender = ::CZTop::Socket::PAIR.new 12 | @receiver = ::CZTop::Socket::PAIR.new 13 | 14 | @addr = "inproc://waker-#{object_id}" 15 | @sender.bind @addr 16 | @receiver.connect @addr 17 | 18 | @sender_lock = Mutex.new 19 | end 20 | 21 | # Wakes up the thread that is waiting for this Waker 22 | def signal 23 | @sender_lock.synchronize do 24 | @sender.signal 25 | end 26 | rescue 27 | raise DeadWakerError, "error sending signal over ZMQ: #{$!.message}" 28 | end 29 | alias_method :wakeup, :signal 30 | 31 | # 0MQ socket to wait for messages on 32 | def socket 33 | @receiver 34 | end 35 | 36 | # Wait for another thread to signal this Waker 37 | def wait 38 | @receiver.wait 39 | rescue 40 | raise DeadWakerError, "error receiving signal over ZMQ: #{$!.message}" 41 | end 42 | 43 | # Clean up the IO objects associated with this waker 44 | def cleanup 45 | @sender_lock.synchronize do 46 | begin 47 | @sender.close 48 | rescue 49 | nil 50 | end 51 | end 52 | begin 53 | @receiver.close 54 | rescue 55 | nil 56 | end 57 | nil 58 | end 59 | alias_method :shutdown, :cleanup 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celluloid/celluloid-zmq/6e4b9eded922880b157b191d53d65e73fb68a9d4/logo.png -------------------------------------------------------------------------------- /spec/celluloid/zmq/actor_spec.rb: -------------------------------------------------------------------------------- 1 | require "celluloid/rspec" 2 | 3 | RSpec.describe Celluloid::ZMQ, library: :ZMQ do 4 | it_behaves_like "a Celluloid Actor", Celluloid::ZMQ 5 | end 6 | -------------------------------------------------------------------------------- /spec/celluloid/zmq/mailbox_spec.rb: -------------------------------------------------------------------------------- 1 | require "celluloid/rspec" 2 | 3 | RSpec.describe Celluloid::ZMQ::Mailbox, library: :ZMQ do 4 | it_behaves_like "a Celluloid Mailbox" 5 | end 6 | -------------------------------------------------------------------------------- /spec/celluloid/zmq/socket_spec.rb: -------------------------------------------------------------------------------- 1 | require "celluloid/rspec" 2 | 3 | RSpec.describe Celluloid::ZMQ::Socket, library: :ZMQ do 4 | it "allows setting and getting ZMQ identity on the socket" do 5 | socket = Celluloid::ZMQ::Socket::Rep.new 6 | socket.identity = "Identity" 7 | 8 | identity = socket.identity 9 | 10 | expect(identity).to eq("Identity") 11 | socket.close 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/celluloid/zmq_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Celluloid::ZMQ, library: :ZMQ do 2 | before { @sockets = [] } 3 | after { @sockets.each(&:close) } 4 | 5 | def connect(socket, index = 0) 6 | socket.connect("inproc://celluloid-spec-#{index}") 7 | @sockets << socket 8 | socket 9 | end 10 | 11 | def bind(socket, index = 0) 12 | socket.bind("inproc://celluloid-spec-#{index}") 13 | @sockets << socket 14 | socket 15 | end 16 | 17 | describe ".init" do 18 | # deprecated 19 | end 20 | 21 | describe Celluloid::ZMQ::Socket::Rep do 22 | let(:actor) do 23 | Class.new do 24 | include Celluloid::ZMQ 25 | 26 | finalizer :close_socket 27 | 28 | def initialize(index) 29 | @socket = Celluloid::ZMQ::Socket::Rep.new 30 | @socket.connect("inproc://celluloid-spec-#{index}") 31 | end 32 | 33 | def say_hi 34 | "Hi!" 35 | end 36 | 37 | def fetch 38 | @socket.read 39 | end 40 | 41 | def close_socket 42 | @socket.close 43 | end 44 | end 45 | end 46 | 47 | it "receives messages" do 48 | server = bind(CZTop::Socket::REQ.new) 49 | client = actor.new(0) 50 | 51 | server << "hello world" 52 | result = client.fetch 53 | expect(result).to eq("hello world") 54 | end 55 | 56 | it "suspends actor while waiting for message" do 57 | server = bind(CZTop::Socket::REQ.new) 58 | client = actor.new(0) 59 | 60 | result = client.future.fetch 61 | expect(client.say_hi).to eq("Hi!") 62 | server << "hello world" 63 | expect(result.value).to eq("hello world") 64 | end 65 | end 66 | 67 | describe Celluloid::ZMQ::Socket::Req do 68 | let(:actor) do 69 | Class.new do 70 | include Celluloid::ZMQ 71 | 72 | finalizer :close_socket 73 | 74 | def initialize(index) 75 | @socket = Celluloid::ZMQ::Socket::Req.new 76 | @socket.connect("inproc://celluloid-spec-#{index}") 77 | end 78 | 79 | def say_hi 80 | "Hi!" 81 | end 82 | 83 | def send(message) 84 | @socket.write(message) 85 | true 86 | end 87 | 88 | def close_socket 89 | @socket.close 90 | end 91 | end 92 | end 93 | 94 | it "sends messages" do 95 | client = bind(CZTop::Socket::REP.new) 96 | server = actor.new(0) 97 | 98 | server.send("hello world") 99 | 100 | message = client.receive[0].to_s 101 | expect(message).to eq("hello world") 102 | end 103 | 104 | it "suspends actor while waiting for message to be sent" do 105 | client = bind(CZTop::Socket::REP.new) 106 | server = actor.new(0) 107 | 108 | result = server.future.send("hello world") 109 | 110 | expect(server.say_hi).to eq("Hi!") 111 | 112 | message = client.receive[0].to_s 113 | expect(message).to eq("hello world") 114 | 115 | expect(result.value).to be_truthy 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | 4 | module Specs 5 | INCLUDED_MODULE = Celluloid::ZMQ 6 | end 7 | 8 | require "celluloid/rspec" 9 | require "celluloid/zmq" 10 | 11 | Dir[*Specs::INCLUDE_PATHS].map { |f| require f } 12 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | require "rspec/core/rake_task" 2 | 3 | RSpec::Core::RakeTask.new 4 | 5 | RSpec::Core::RakeTask.new(:rcov) do |task| 6 | task.rcov = true 7 | end 8 | -------------------------------------------------------------------------------- /tasks/rubocop.rake: -------------------------------------------------------------------------------- 1 | unless ENV["CI"] 2 | require "rubocop/rake_task" 3 | RuboCop::RakeTask.new 4 | end 5 | --------------------------------------------------------------------------------