A Narrative for Users
This document explores the OnStomp
API through a narrative aimed at end
users of the library. It will start with the basics and work through the
available features through exposition and examples. It may be helpful to
review the STOMP specification before
diving into this document. It's also important to note that onstomp
can
only be used with Ruby 1.8.7+. Support for Rubies prior to 1.8.7 does not
exist, and even requiring the library in your code will probably generate
errors.
Creating a STOMP Client
Creating a client connection to a STOMP broker is done by creating a new client and connecting it. This can be accomplished a few different ways.
require 'onstomp' # The common way client = OnStomp::Client.new("stomp://host.example.org") client.connect # A short-cut client = OnStomp.connect "stomp://host.example.org"
The OnStomp.connect method creates a new client instance and immediately
calls connect on it. This method is also aliased as
open
, so use the verbiage you're most comfortable with.
Once connected, frames can be sent to the STOMP broker through a series of convenient (and fairly common amongst most STOMP clients) methods such as send, subscribe and ack. A full list of the frame methods can be found in the documentation for the OnStomp::Interfaces::FrameMethods mixin.
So, let's send some SEND frame to the broker:
client.send '/queue/test', 'Hello World!' client.send '/queue/test', 'Persist this, please.', :persistent => true
Most frame-generating methods treat the last parameter as a hash of headers to include with the generated frame. The only exception to this is the heart-beat frame, which has no command, headers or body.
Subscriptions and Receipts
Subscribing: Send me stuff, and maybe I'll tell you when I got it.
Subscriptions in onstomp
are pretty much just blocks that get called every
time a MESSAGE frame is received that matches a previously sent SUBSCRIBE frame.
To set up a subscription, just pass a block to the subscribe method:
client.subscribe '/queue/test' do |msg| # Invoked every time the broker delivers a MESSAGE frame for the # SUBSCRIBE frame generated by this method call. puts "Got a message: #{msg.body}" end
The STOMP protocol supports a few different ways of acknowledging that MESSAGE
frames were received, depending upon the protocol version. STOMP 1.0
connections support automatic acknowledgment (the default behavior) and
client-side message acknowledgment. STOMP 1.1 adds a client-individual
mode
that may behave differently depending upon the broker you are using. It is
considered incorrect for a client to acknowledge MESSAGE frames with ACK
frames if the subscription is operating in auto
mode. To set the ack mode
of a subscription, include an :ack
header in your call to
subscribe:
# Technically, this isn't needed as auto is the default ack mode client.subscribe '/queue/test', :ack => 'auto' do |msg| # process the MESSAGE frame # ... end # Set the subscription's ack mode to client client.subscribe '/queue/test', :ack => 'client' do |msg| # process the MESSAGE frame # ... # Tell the broker that the MESSAGE frame was processed client.ack msg end # Set the subscription's ack mode to client-individual client.subscribe '/queue/test', :ack => 'client-individual' do |msg| # process the MESSAGE frame # ... # Tell the broker that the MESSAGE frame was NOT processed client.nack msg end
The difference between :ack => 'client'
and :ack => 'client-individual
largely depends upon the STOMP broker. Apache's ActiveMQ
treats ACK frames received for a MESSAGE frame as "cumulative acknowledgements,"
that is an ACK frame acknowledges the MESSAGE it was sent for and all previous
MESSAGE frames sent from the broker to the client. The STOMP 1.1 spec
clarified the expected behavior brokers should exhibit when receiving an ACK
frame, and a client-individual
ack mode specifies that each MESSAGE frame
will be acknowledged with its own ACK (or NACK) frame. There may be brokers
that behave this way when using a client
ack mode, so what happens when
you ACK a MESSAGE in client
mode depends heavily on the broker being used.
The NACK frame was introduced in the STOMP 1.1 spec and gives the client a way to tell the broker that it did not successfully process a MESSAGE. It is very similar in structure to an ACK frame, which makes sense it is little more than a "Not ACK".
Receipts: Did you get that thing I sent you?
Most frames a client sends to a STOMP broker can be receipted (ie: the
client can instruct the broker to send a RECEIPT frame after it receives
the original frame.) The two exceptions to this are heart-beat frames (as
mentioned previously, heart-beats really aren't frames) and CONNECT
frames. The client does this by including a receipt
header that specifies
an receipt ID for the frame being sent, the broker will in turn deliver a
RECEIPT frame with a matching receipt-id
header. In OnStomp, requesting a
receipt for a SEND frame is as easy as including a block with your call to
send:
client.send '/queue/test', 'Did you get this?' do |r| puts "Got my receipt: #{r[:receipt-id']}" end
To request receipts for other types of frames, see with_receipt subsection of Scopes.
Scopes
Sometimes you want to do the same stuff with a series of frames, and that's why we have scopes.
with_headers
A header scope is a convenient way to apply a common set of headers to a series of frames. You can create a new header scope from a client by calling with_headers:
scope = client.with_headers :persistent => true, :content-type' => 'text/plain' scope.send '/queue/test', 'walks in to the room' scope.send '/queue/test', 'feels like a big balloon', :persitent => false scope.send '/queue/test', 'big girls, you are beautiful'
All of the SEND frames generated will have a content-type
header with a value
of text/plain
. The first and last frames will also have a header of
persistent
with a value of true
; however, the middle frame's persistent
header will have a value of false
. As illustrated, headers specified on
the frame-generating methods will override those defined for the scope.
If you want to apply the headers to a series of frames in one swoop and don't
need to keep a scope
variable around, you can pass a block to with_headers
:
client.with_headers :persistent => true, :content-type' => 'text/plain' do |h| h.send '/queue/test', 'walks in to the room' h.send '/queue/test', 'feels like a big balloon', :persitent => false h.send '/queue/test', 'big girls, you are beautiful' end
This code sample produces the same results as the earlier example but without the need to keep track of the header scope instance.
with_receipt
A receipt scope is a convenient way to use the same receipt callback for multiple receipts. You can create a new receipt scope from a client by calling with_receipt with the shared callback:
scope = client.with_receipt do |r| puts "Got my receipt!" end scope.send '/queue/test', 'walks in to the room' scope.subscribe '/queue/test2'
This code sample will instruct the broker to create RECEIPT frames for client
generated SEND and SUBSCRIBE frames. The receipt scope takes care of generating
unique values for the receipt
header of each frame. If you only need the
receipt handler for one frame, you can use a bit of method chaining:
client.with_receipt do |r| puts "Broker got DISCONNECT" end.disconnect
transaction
A transaction scope is a little
more complicated than the previous scopes, but only marginally so. This scope
is useful if you want to deliver some frames as part of a transaction, but
don't want to be bothered with managing transaction
headers manually.
The simplest way to use a transaction scope is to hand it a block you want
handled transactionally:
client.transaction do |t| t.send '/queue/test', 'one of three' t.send '/queue/test', 'two of three' t.send '/queue/test', 'three of three' end
When passed a block, the transaction scope will automatically transmit a
BEGIN frame, deliver all transactional frames in the block with a matching
transaction
header and then send a COMMIT
frame to complete the
transaction. If an error is raised within the block, an ABORT frame will be
sent to the broker to roll-back the transaction and the offending error
will be re-raised.
Transaction scopes also support being re-used, but a little more work is required on your part:
trans = client.transaction trans.begin trans.send '/queue/test', 'one of three' trans.send '/queue/test', 'two of three' trans.send '/queue/test', 'three of three' trans.commit # First transaction is complete trans.begin trans.send '/queue/other', 'next transaction' trans.abort # Second transaction is rolled-back trans.begin trans.send '/queue/yet-another', 'last transaction' trans.commit
When used like this, the transaction scope will automatically generate a new
transaction id with each call to
begin, but you must
manually begin and end the transactions. If you attempt to transmit a frame
as part of a transaction that was already committed or aborted, the frame
will be sent to the broker, but will not be a part of any transaction (ie:
it will not have a transaction
header.) This is also the case for frames
that cannot be transacted (eg: SUBSCRIBE, UNSUBSCRIBE.)
Events and Callbacks
A key feature of the onstomp
gem is the
event-driven interface. A sufficient set
of events can be bound to fully monitor what frames are being sent or received
or event what frame information is ultimately delivered to the broker. There
are two event hooks for every type of STOMP frame, one prefixed with
before_
, the other prefixed with on_
. The difference between the two
is when they are triggered:
client.before_send do |frame, client_obj| # In here, frame is the SEND frame to deliver to the broker and # client_obj == client. All frame-based event callbacks are passed # these two parameters. puts "SEND frame will be sent, but hasn't been sent yet!" frame[:a_header] = 'a header set in an event callback' end client.on_send do |frame, client_obj| puts "SEND frame was delivered to the broker: #{frame[:a_header]}" # By now, the SEND frame has already been delivered to the broker, # so the following line does not change what the broker received, # but the change will be picked up by all other `on_send` callbacks # registered after this one. frame[:a_header] = 'a header changed in an event callback' end
Internally, onstomp
uses non-blocking IO calls to read from and write to
the STOMP broker (for more details, check out OnStomp::Connections::Base.)
When dealing with client-generated frames (eg: SEND, SUBSCRIBE, DISCONNECT),
the before_<frame>
and
before_transmitting
events are triggered after a frame has been queued in the write buffer. Once
the frame has actually been written to the underlying TCP/IP socket, the
after_transmitting
and on_<frame>
events are triggered.
Below is list illustrating the sequence client related frame events are
triggered:
- You call
client.send ...
and a SEND frame is created - The event
before_transmitting
is triggered for the SEND frame - The event
before_send
is triggered for the SEND frame - The SEND frame is added to the connection's write buffer.
- Some amount of time passes (perhaps a little, perhaps a lot depending on the IO load between you and the broker)
- The SEND frame is serialized and fully written to the broker.
- The event
after_transmitting
is triggered for the SEND frame - The event
on_send
is triggered for the SEND frame. - The frame delivery process is now complete!
When broker generated frames (eg: MESSAGE, ERROR, RECEIPT) are received,
the corresponding before_<frame>
and before_receiving
events are
triggered, followed immediately by the triggering of the on_<frame>
and
after_receiving
events.
Below is a list illustrating the sequence broker related frame events are
triggered:
- The broker writes a MESSAGE frame to the TCP/IP socket.
- Some amount of time passes (perhaps a little, perhaps a lot depending on the IO load between you and the broker)
- The client fully reads and de-serializes the MESSAGE frame
- The event
before_receiving
is triggered for the MESSAGE frame - The event
before_message
is triggered for the MESSAGE frame - The event
after_receiving
is triggered for the MESSAGE frame - The event
on_message
is triggered for the MESSAGE frame - The frame receiving process is now complete!
Unlike transmitted frames, nothing special happens between before_receiving
and after_receiving
, these event prefixes exist to help ease order of
execution issues you may have with received frames.
In addition to all of the frame-related events, there are a few connection
related events that are triggered by changes in the connection between
you and the STOMP broker: on_connection_established
, on_connection_died
,
on_connection_terminated
, and on_connection_closed
. These are mostly just
wrappers around the similarly named
connection events, with the added
bonus that they can be bound before the client has created a connection (for
more details on the difference between a client and a connection, see
the On Clients and Connections
subsection of the Appendix.) What follows
is a brief run-down of these events:
on_connection_establised
- triggered when a socket to the broker has been created and the CONNECT/CONNECTED frame exchange has taken place.on_connection_died
- triggered by STOMP 1.1 connections when the agreed upon heart-beating rate has not been met by either the broker or the client.on_connection_terminated
- triggered when the connection is closed unexpectedly (ie: when reading or writing to the socket raises an exception.)on_connection_closed
- triggered any time the socket is closed.
All connection event callbacks will be invoked with two parameters: the client and the connection, respectively.
Body Encodings
The STOMP 1.1 protocol allows users to encode the bodies of frames and notify
the broker (and other clients) of the encoding within the content-type
header. OnStomp tries to do the right thing for you, but only if you're using
Ruby 1.9+. Before I get into that, I'm going to first talk about what
happens if you're using Ruby 1.8.7.
Prior to version 1.9, Ruby treated strings as a collection of bytes without
paying any mind to character encodings. As a result of this, if you are
connected to a STOMP 1.1 broker, it will be up to you to set content-type
header and its charset
parameter appropriately. The good news is, if you
don't specify a charset header, STOMP 1.1 dictates that the frame's body
should be treated as binary data so at least the broker shouldn't choke
on your SEND frames. Further, if your frame bodies contain ASCII or UTF-8
text, you can set the content-type
header to 'text/whatever', and ignore
its charset
parameter because all frames that have a content-type
header
with a text
type default to a UTF-8 encoding when charset
is not specified.
You should be aware that any MESSAGE frames the broker sends to you
may contain bodies with various character encodings that Ruby will treat as
a collection of bytes. The last thing to be aware of, is that STOMP 1.1
requires headers are UTF-8 encoded, so only use UTF-8 characters in your header
names and values or incur the wrath of your STOMP broker. You've been warned!
If you're using Ruby 1.9+, most of the work will be done for you, but there is
one potential "gotcha." First off, the good stuff: use whatever encoding
you like for your headers and your frame bodies. As long as the headers
can be cleanly translated to UTF-8, onstomp
will automatically do the work
for you so the broker receives the UTF-8 encoded headers it expects.
Furthermore, use whatever Ruby supported encoding you like for your SEND
frames and onstomp
will make sure content-type
and its charset
header
get set accordingly. But wait, there's more! When the broker sends you a frame
with a body using a Ruby supported encoding, you can rest easy knowing that
frame.body.encoding
will be there handling your big beautiful character
encodings. And now that you're sitting there, content in the knowledge that
onstomp
does so much for you, it's time to hit you with the "gotcha." If
the body of your SEND frame is meant to be treated as binary data, you'll
need to make sure your string is using the ASCII-8BIT (aka: BINARY) encoding.
This should be the case if you read your data from a file, but almost
certainly won't be the case if you're data is a literal string inside your
code. If your body string does not have a binary encoding, onstomp
will
assume that the body is plain text and will set the charset
parameter
of the content-type
header, which you probably don't want if you really
are working with binary data.
Finishing Up
After you're all done with your messaging exchange, make sure to disconnect!
# This ensures that all buffered data is sent to the broker client.disconnect
For more information on why it is so important to disconnect your clients, please read the next section.
Appendix
What Really Goes Down when you .disconnect
If there are a lot of frames being exchanged between you and a STOMP broker,
you may notice that calling client.disconnect
seems to hang. That's because
it does! Calling disconnect forces a client's
connection to write all of the data in its write buffer to broker before
going any further. By default, onstomp
uses a separate thread to perform
both reading and writing IO operations to keep data moving quickly. This
approach has one significant draw-back: you could write a program that
delivers a few SENDs, reads a bunch of MESSAGEs and then exits only to
discover that not all of your SENDs actually got sent. That is because the
main thread of your program terminated before the IO thread ever did its
thing. Fortunately, I don't want you to have to worry about threaded
non-blocking IO, so I made disconnect special.
As long as you call disconnect
on your client before your program finishes,
every frame you told your client to deliver will be written to the broker.
The sometimes noticeable side-effect of this is that if there is a lot of
traffic between your client and the STOMP broker, the write buffer may
be pretty full when you call disconnect
, which means it will take some
time for all those frames to get flushed. In testing, I found this becomes
most noticeable when the STOMP broker is sending lots and lots of frames
(I was deliberately causing the broker to generate an ERROR frame for each
of 5,000 frames I sent to it.)
On Clients and Connections
A single client implementation works with both STOMP 1.0 and STOMP 1.1 protocols (and, hopefully, with any future STOMP protocol.) This is made possible by the goodness that is composition: each client creates an instance of a connection when it is told to connect to its broker. The connections do the protocol-specific work of generating supported frames with their necessary components. At present, there are two concrete connection classes, one for STOMP 1.0 and one for STOMP 1.1. The changes between STOMP 1.0 and STOMP 1.1 were meant to be largely backwards compatible, and so all of the common functionality for these connections is contained in the Stomp_1 module.
With any luck, implementing future versions of the STOMP protocol will only require creating a new connection subclass that "does the right thing" and a bit of fiddling with the OnStomp::Connections module to register the protocol and possibly tweak how protocol negotiation goes down.
The open-uri
Angle
The code to support open-uri
style STOMP interaction is a direct port of
the code used in the deprecated stomper
gem. While I've tested it and it seems
to be working just fine with onstomp
, I'd like to review the code a bit more
before I'll call it anything other than experimental. However, if you want
to try it out, you can do so with require 'onstomp/open-uri'
:
require 'onstomp' # This will automatically require open-uri from Ruby's stdlib. require 'onstomp/open-uri' open('stomp://host.example.org/queue/onstomp/open-uri-test') do |c| c.send 'Hello from open-uri!' c.send 'Another pointless message coming at ya!' c.send 'big girls, you are beautiful' c.each do |m| puts "Got a message: #{m.body}" break end c.first(2) # => [ MESSAGE FRAME ('Another pointless..'), # MESSAGE FRAME ('big girls, you ...') ] end
Again, for now this feature is experimental, but if you're using it and find any bugs, don't hesitate to report them in the issue tracker
Failing Over
This is another experimental feature of onstomp
that was a port of a
stomper
feature. This extension adds failover / reliability support to
your communications with a STOMP broker. The same caveats of the open-uri
extension apply here and feel free to report any bugs you find in the
issue tracker. If you want
to make use of the failover features, you can do so with
require 'onstomp/failover'
:
require 'onstomp' require 'onstomp/failover' client = OnStomp::Failover::Client.new 'failover:(stomp://host1.example.org,stomp+ssl://host2.example.org)' client.subscribe '/queue/test' do |m| puts "Got a message: #{m.body}" end client.send '/queue/test', 'Hello from failover!' client.send '/queue/test', 'Another message coming from failover!' client.disconnect
You can create a failover client by using a 'failover:' URI or an array of
standard URIs. It is very important, however, that any 'failover:' URIs follow
the above pattern. Using a URI such as
failover://stomp://host1.example.org,stomp://host2.example.org
or even
failover://(stomp://host1.example.org,stomp://host2.example.org)
will produce
a parsing error at this time. In future releases I hope to make the failover
URI parser a bit more robust, but for the time being only URIs of the form:
failover:(uri1,uri2,uri3)?param1=value1¶m2=value2