diff options
Diffstat (limited to 'qpid/ruby/lib/qpid')
25 files changed, 13198 insertions, 0 deletions
diff --git a/qpid/ruby/lib/qpid/assembler.rb b/qpid/ruby/lib/qpid/assembler.rb new file mode 100644 index 0000000000..b768c3f195 --- /dev/null +++ b/qpid/ruby/lib/qpid/assembler.rb @@ -0,0 +1,148 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 Qpid + + class << self + attr_accessor :asm_logger + end + + class Segment + + attr_reader :type, :payload, :track, :channel + attr_accessor :id, :offset + + def initialize(first, last, type, track, channel, payload) + @id = nil + @offset = nil + @first = first + @last = last + @type = type + @track = track + @channel = channel + @payload = payload + end + + def first_segment? ; @first ; end + + def last_segment? ; @last ; end + + def decode(spec) + segs = spec[:segment_type] + choice = segs.enum.choices[type] + return method("decode_#{choice.name}").call(spec) + end + + def decode_control(spec) + sc = StringCodec.new(spec, payload) + return sc.read_control() + end + + def decode_command(spec) + sc = StringCodec.new(spec, payload) + hdr, cmd = sc.read_command() + cmd.id = id + return hdr, cmd + end + + def decode_header(spec) + sc = StringCodec.new(spec, payload) + values = [] + until sc.encoded.empty? + values << sc.read_struct32() + end + return values + end + + def decode_body(spec) + payload + end + + def append(frame) + @payload += frame.payload + end + + def to_s + f = first_segment? ? 'F' : '.' + l = last_segment? ? 'L' : '.' + return "%s%s %s %s %s %s" % [f, l, @type, + @track, @channel, @payload.inspect] + end + + end + + class Assembler < Framer + + def logger; Qpid::asm_logger; end + + def initialize(sock, max_payload = Frame::MAX_PAYLOAD) + super(sock) + @max_payload = max_payload + @fragments = {} + end + + def read_segment + loop do + frame = read_frame + key = [frame.channel, frame.track] + seg = @fragments[key] + unless seg + seg = Segment.new(frame.first_segment?, + frame.last_segment?, + frame.type, frame.track, + frame.channel, "") + @fragments[key] = seg + end + + seg.append(frame) + + if frame.last_frame? + @fragments.delete(key) + logger.debug("RECV #{seg}") if logger + return seg + end + end + end + + def write_segment(segment) + remaining = segment.payload + + first = true + while first or remaining + payload = remaining[0, @max_payload] + remaining = remaining[@max_payload, remaining.size] + + flags = 0 + + flags |= FIRST_FRM if first + flags |= LAST_FRM unless remaining + flags |= FIRST_SEG if segment.first_segment? + flags |= LAST_SEG if segment.last_segment? + + frame = Frame.new(flags, segment.type, segment.track, + segment.channel, payload) + write_frame(frame) + + first = false + end + + logger.debug("SENT #{segment}") if logger + end + end +end diff --git a/qpid/ruby/lib/qpid/client.rb b/qpid/ruby/lib/qpid/client.rb new file mode 100644 index 0000000000..ec3d100a9c --- /dev/null +++ b/qpid/ruby/lib/qpid/client.rb @@ -0,0 +1,136 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "thread" +require "qpid/peer" +require "qpid/queue" + +module Qpid08 + + class Client + def initialize(host, port, spec, vhost = "/") + @host = host + @port = port + @spec = spec + @vhost = vhost + + @mechanism = nil + @response = nil + @locale = nil + + @queues = {} + @mutex = Mutex.new() + + @closed = false + @code = nil + @started = ConditionVariable.new() + + @conn = Connection.new(@host, @port, @spec) + @peer = Peer.new(@conn, ClientDelegate.new(self)) + end + + attr_reader :mechanism, :response, :locale + + def closed?; @closed end + def closed=(value); @closed = value end + def code; @code end + + def wait() + @mutex.synchronize do + @started.wait(@mutex) + end + raise EOFError.new() if closed? + end + + def signal_start() + @started.broadcast() + end + + def queue(key) + @mutex.synchronize do + q = @queues[key] + if q.nil? + q = Queue.new() + @queues[key] = q + end + return q + end + end + + def start(response, mechanism="AMQPLAIN", locale="en_US") + @response = response + @mechanism = mechanism + @locale = locale + + @conn.connect() + @conn.init() + @peer.start() + wait() + channel(0).connection_open(@vhost) + end + + def channel(id) + return @peer.channel(id) + end + + def close(msg = nil) + @closed = true + @code = msg + @peer.close() + end + end + + class ClientDelegate + + include Delegate + + def initialize(client) + @client = client + end + + def connection_start(ch, msg) + ch.connection_start_ok(:mechanism => @client.mechanism, + :response => @client.response, + :locale => @client.locale) + end + + def connection_tune(ch, msg) + ch.connection_tune_ok(*msg.fields) + @client.signal_start() + end + + def connection_close(ch, msg) + puts "CONNECTION CLOSED: #{msg.args.join(", ")}" + @client.close(msg) + end + + def channel_close(ch, msg) + puts "CHANNEL[#{ch.id}] CLOSED: #{msg.args.join(", ")}" + ch.channel_close_ok() + ch.close() + end + + def basic_deliver(ch, msg) + queue = @client.queue(msg.consumer_tag) + queue << msg + end + + end + +end diff --git a/qpid/ruby/lib/qpid/codec.rb b/qpid/ruby/lib/qpid/codec.rb new file mode 100644 index 0000000000..a3b5d101c4 --- /dev/null +++ b/qpid/ruby/lib/qpid/codec.rb @@ -0,0 +1,457 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 'qpid/packer.rb' +require 'iconv' + +module Qpid + + class Codec + + include Qpid::Packer + + attr_reader :spec + + def initialize(spec = "") + @spec = spec + end + + def write_void(v) + unless v.nil? + raise Exception.new("void not nil: #{v}") + end + end + + def read_void + return nil + end + + def write_bit(b) + unless b + raise Exception.new("bit is nil: #{b}") + end + end + + def read_bit + return true + end + + def read_uint8 + return unpack("C", 1) + end + + def write_uint8(n) + return pack("C", n) + end + + def read_int8 + return unpack("c", 1) + end + + def write_int8(n) + pack("c", n) + end + + def read_char + return unpack("c", 1) + end + + def write_char(c) + pack("c") + end + + def read_boolean + return read_uint8 != 0 + end + + def write_boolean(b) + n = 0 + n = 1 if b != 0 + write_uint8(n) + end + + def read_uint16 + return unpack("n", 2) + end + + def write_uint16(n) + pack("n", n) + end + + def read_int16 + # XXX: holy moly.. pack/unpack doesn't have signed network byte order. Crazy hackery. + val = unpack("n", 2) + val -= 2 ** 16 if val >= 2 ** 15 + return val + end + + def write_int16(n) + # XXX: Magically this one works even though it's not signed. + pack("n", n) + end + + def read_uint32 + return unpack("N", 4) + end + + def write_uint32(n) + pack("N", n) + end + + def read_int32 + # Again no pack/unpack for signed int + return unpack("N", 4) + end + + def write_int32(n) + # FIXME + pack("N", n) + end + + def read_float + return unpack("g", 4) + end + + def write_float(n) + pack("g", n) + end + + def read_sequence_no + return read_uint32.to_serial + end + + def write_sequence_no(n) + write_uint32(n.value) + end + + def encode_64bit(num, signed = false) + b = [] + + if num < 0 && signed + num += 2 ** 64 + end + + (0..7).each do |c| + d = 7 - c + b[c] = (num & (0xff << d * 8)) >> d * 8 + end + pack('C8', *b) + end + + + def decode_64bit(signed = false) + # Silly ruby pack/unpack does not implement 64 bit network byte order + # encode/decode. + a = unpack('C8', 8) + num = 0 + (0..7).each do |c| + d = 7 - c + num |= a[c] << 8 * d + end + + if signed && num >= 2 ** 63 + num -= 2 ** 64 + end + return num + end + + def read_uint64 + return decode_64bit + end + + def write_uint64(n) + encode_64bit(n) + end + + def read_int64 + return decode_64bit(signed = true) + end + + def write_int64(n) + encode_64bit(n, signed = true) + end + + def read_datetime + return read_uint64 + end + + def write_datetime(n) + write_uint64(n) + end + + def read_double + return unpack("G", 8) + end + + def write_double(n) + pack("G", n) + end + + def read_vbin8 + # XXX + return read(read_uint8) + end + + def write_vbin8(b) + # XXX + write_uint8(b.length) + write(b) + end + + def read_str8 + # FIXME: Check iconv.. I think this will throw if there are odd characters. + return Iconv.conv("ASCII", "UTF-8", read_vbin8) + end + + def write_str8(s) + write_vbin8(Iconv.conv("UTF-8", "ASCII", s)) + end + + def read_str16 + return Iconv.conv("ASCII", "UTF-8", read_vbin16) + end + + def write_str16(s) + write_vbin16(Iconv.conv("UTF-8", "ASCII", s)) + end + + def read_vbin16 + # XXX: Using read method? + return read(read_uint16) + end + + def write_vbin16(b) + write_uint16(b.length) + write(b) + end + + def read_sequence_set + # FIXME: Need datatypes + result = RangedSet.new + size = read_uint16 + nranges = size / 8 + nranges.times do |i| + lower = read_sequence_no + upper = read_sequence_no + result.add(lower, upper) + end + return result + end + + def write_sequence_set(ss) + size = 8 * ss.ranges.length + write_uint16(size) + ss.ranges.each do |range| + write_sequence_no(range.lower) + write_sequence_no(range.upper) + end + end + + def read_vbin32 + return read(read_uint32) + end + + def write_vbin32(b) + write_uint32(b.length) + write(b) + end + + def write_map(m) + sc = StringCodec.new(@spec) + unless m.nil? + sc.write_uint32(m.size) + m.each do |k, v| + unless type = @spec.encoding(v.class) + raise Exception.new("no encoding for: #{v.class}") + end + sc.write_str8(k) + sc.write_uint8(type.code) + type.encode(sc, v) + end + end + write_vbin32(sc.encoded) + end + + def read_map + sc = StringCodec.new(@spec, read_vbin32) + return nil unless sc.encoded + count = sc.read_uint32 + result = nil + if count + result = {} + until sc.encoded.empty? + k = sc.read_str8 + code = sc.read_uint8 + type = @spec.types[code] + v = type.decode(sc) + result[k] = v + end + end + return result + end + + def write_array(a) + sc = StringCodec.new(@spec) + unless a.nil? + if a.length > 0 + type = @spec.encoding(a[0].class) + else + type = @spec.encoding(nil.class) + end + sc.write_uint8(type.code) + sc.write_uint32(a.size) + a.each { |o| type.encode(sc, o) } + end + write_vbin32(sc.encoded) + end + + def read_array + sc = StringCodec.new(@spec, read_vbin32) + return nil if not sc.encoded + type = @spec.types[sc.read_uint8] + count = sc.read_uint32 + result = nil + if count + result = [] + count.times { |i| result << (type.decode(sc)) } + end + return result + end + + def write_list(l) + sc = StringCodec.new(@spec) + unless l.nil? + sc.write_uint32(l.length) + l.each do |o| + type = @spec.encoding(o.class) + sc.write_uint8(type.code) + type.encode(sc, o) + end + end + write_vbin32(sc.encoded) + end + + def read_list + sc = StringCodec.new(@spec, read_vbin32) + return nil if not sc.encoded + count = sc.read_uint32 + result = nil + if count + result = [] + count.times do |i| + type = @spec.types[sc.read_uint8] + result << type.decode(sc) + end + end + return result + end + + def read_struct32 + size = read_uint32 + code = read_uint16 + type = @spec.structs[code] + # XXX: BLEH! + fields = type.decode_fields(self) + return Qpid::struct(type, fields) + end + + def write_struct32(value) + type = value.st_type + sc = StringCodec.new(@spec) + sc.write_uint16(type.code) + type.encode_fields(sc, value) + write_vbin32(sc.encoded) + end + + def read_control + cntrl = @spec.controls[read_uint16] + return Qpid::struct(cntrl, cntrl.decode_fields(self)) + end + + def write_control(ctrl) + type = ctrl.st_type + write_uint16(type.code) + type.encode_fields(self, ctrl) + end + + def read_command + type = @spec.commands[read_uint16] + hdr = @spec[:header].decode(self) + cmd = Qpid::struct(type, type.decode_fields(self)) + return hdr, cmd + end + + def write_command(hdr, cmd) + type = cmd.st_type + write_uint16(type.code) + hdr.st_type.encode(self, hdr) + type.encode_fields(self, cmd) + end + + def read_size(width) + if width > 0 + return send(:"read_uint#{width * 8}") + end + end + + def write_size(width, n) + if width > 0 + send(:"write_uint#{width * 8}", n) + end + end + + def read_uuid + return unpack("a16", 16) + end + + def write_uuid(s) + pack("a16", s) + end + + def read_bin128 + return unpack("a16", 16) + end + + def write_bin128(b) + pack("a16", b) + end + + end + + class StringCodec < Codec + + def initialize(spec, encoded = "") + @spec = spec + @encoded = encoded + end + + attr_reader :encoded + + def write(s) + @encoded += s + end + + def read(n) + return "" if n.nil? + result = @encoded[0...n] + @encoded = @encoded[n...@encoded.size] || "" + return result + end + end +end diff --git a/qpid/ruby/lib/qpid/codec08.rb b/qpid/ruby/lib/qpid/codec08.rb new file mode 100644 index 0000000000..148dee07bb --- /dev/null +++ b/qpid/ruby/lib/qpid/codec08.rb @@ -0,0 +1,265 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 Qpid08 + # is there a better way to do this? + class StringWriter + + def initialize(str = "") + @str = str + end + + def write(value) + @str << value + end + + def to_s() + return @str + end + + end + + class EOF < Exception; end + + class Encoder + + def initialize(out) + @out = out + @bits = [] + end + + attr_reader(:out) + + def encode(type, value) + send(type, value) + end + + def bit(b) + @bits << b + end + + def octet(o) + pack("C", o) + end + + def short(s) + pack("n", s) + end + + def long(l) + pack("N", l) + end + + def longlong(l) + lower = l & 0xffffffff + upper = (l & ~0xffffffff) >> 32 + long(upper) + long(lower) + end + + def timestamp(l) + longlong(l) + end + + def shortstr(s) + # shortstr is actually octetstr + octet(s.length) + write(s) + end + + def longstr(s) + case s + when Hash + table(s) + else + long(s.length) + write(s) + end + end + + def table(t) + t = {} if t.nil? + enc = Encoder.new(StringWriter.new()) + t.each {|key, value| + enc.shortstr(key) + # I offer this chicken to the gods of polymorphism. May they + # choke on it. + case value + when String + type = :longstr + desc = "S" + when Numeric + type = :long + desc = "I" + else + raise Exception.new("unknown table value: #{value.class}") + end + enc.write(desc) + enc.encode(type, value) + } + longstr(enc.out.to_s()) + end + + def write(str) + flushbits() + @out.write(str) + # puts "OUT #{str.inspect()}" + end + + def pack(fmt, *args) + write(args.pack(fmt)) + end + + def flush() + flushbits() + end + + private + + def flushbits() + if @bits.empty? then return end + + bytes = [] + index = 0 + @bits.each {|b| + bytes << 0 if index == 0 + if b then bytes[-1] |= 1 << index end + index = (index + 1) % 8 + } + @bits.clear() + bytes.each {|b| + octet(b) + } + end + + end + + class StringReader + + def initialize(str) + @str = str + @index = 0 + end + + def read(n) + result = @str[@index, n] + @index += result.length + return result + end + + end + + class Decoder + + def initialize(_in) + @in = _in + @bits = [] + end + + def decode(type) + return send(type) + end + + def bit() + if @bits.empty? + byte = octet() + 7.downto(0) {|i| + @bits << (byte[i] == 1) + } + end + return @bits.pop() + end + + def octet() + return unpack("C", 1) + end + + def short() + return unpack("n", 2) + end + + def long() + return unpack("N", 4) + end + + def longlong() + upper = long() + lower = long() + return upper << 32 | lower + end + + def timestamp() + return longlong() + end + + def shortstr() + # shortstr is actually octetstr + return read(octet()) + end + + def longstr() + return read(long()) + end + + def table() + dec = Decoder.new(StringReader.new(longstr())) + result = {} + while true + begin + key = dec.shortstr() + rescue EOF + break + end + desc = dec.read(1) + case desc + when "S" + value = dec.longstr() + when "I" + value = dec.long() + else + raise Exception.new("unrecognized descriminator: #{desc.inspect()}") + end + result[key] = value + end + return result + end + + def read(n) + return "" if n == 0 + result = @in.read(n) + if result.nil? or result.empty? + raise EOF.new() + else + # puts " IN #{result.inspect()}" + return result + end + end + + def unpack(fmt, size) + result = read(size).unpack(fmt) + if result.length == 1 + return result[0] + else + return result + end + end + + end + +end diff --git a/qpid/ruby/lib/qpid/config.rb b/qpid/ruby/lib/qpid/config.rb new file mode 100644 index 0000000000..b5b79cd309 --- /dev/null +++ b/qpid/ruby/lib/qpid/config.rb @@ -0,0 +1,32 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 Qpid + module Config + + def self.amqp_spec + dirs = [File::expand_path(File::join(File::dirname(__FILE__), "specs"))] + dirs.each do |d| + spec = File::join(d, "amqp.0-10-qpid-errata.xml") + return spec if File::exists? spec + end + end + + end +end diff --git a/qpid/ruby/lib/qpid/connection.rb b/qpid/ruby/lib/qpid/connection.rb new file mode 100644 index 0000000000..d2efbfb263 --- /dev/null +++ b/qpid/ruby/lib/qpid/connection.rb @@ -0,0 +1,222 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 'monitor' + +module Qpid + + class ChannelBusy< Exception ; end + + class ChannelsBusy < Exception ; end + + class SessionBusy < Exception ; end + + class ConnectionFailed < Exception ; end + + class Timeout < Exception ; end + + class Connection < Assembler + + include MonitorMixin + + attr_reader :spec, :attached, :sessions, :thread + attr_accessor :opened, :failed, :close_code, :user_id + + def initialize(sock, args={}) + super(sock) + + delegate = args[:delegate] || Qpid::Delegate::Client.method(:new) + spec = args[:spec] || nil + + @spec = Qpid::Spec010::load(spec) + @track = @spec["track"] + + @attached = {} + @sessions = {} + + @condition = new_cond + @opened = false + @failed = false + @close_code = [nil, "connection aborted"] + + @thread = nil + + @channel_max = 65535 + @user_id = nil + + @delegate = delegate.call(self, args) + end + + def attach(name, ch, delegate, force=false) + synchronize do + ssn = @attached[ch.id] + if ssn + raise ChannelBusy.new(ch, ssn) unless ssn.name == name + else + ssn = @sessions[name] + if ssn.nil? + ssn = Session.new(name, @spec, :delegate => delegate) + @sessions[name] = ssn + elsif ssn.channel + if force + @attached.delete(ssn.channel.id) + ssn.channel = nil + else + raise SessionBusy.new(ssn) + end + end + @attached[ch.id] = ssn + ssn.channel = ch + end + ch.session = ssn + return ssn + end + end + + def detach(name, ch) + synchronize do + @attached.delete(ch.id) + ssn = @sessions.delete(name) + if ssn + ssn.channel = nil + ssn.closed + return ssn + end + end + end + + def session(name, kwargs = {}) + timeout = kwargs[:timeout] + delegate = kwargs[:delegate] || Qpid::Session::Client.method(:new) + + # FIXME: Python has cryptic comment about 'ch 0 ?' + channel = (0..@channel_max).detect { |i| ! @attached.key?(i) } + raise ChannelsBusy unless channel + + synchronize do + ch = Channel.new(self, channel) + ssn = attach(name, ch, delegate) + ssn.channel.session_attach(name) + if ssn.wait_for(timeout) { ssn.channel } + return ssn + else + detach(name, ch) + raise Timeout + end + end + end + + def detach_all + synchronize do + attached.values.each do |ssn| + ssn.exceptions << @close_code unless @close_code[0] == 200 + detach(ssn.name, ssn.channel) + end + end + end + + def start(timeout=nil) + @delegate.start + @thread = Thread.new { run } + @thread[:name] = 'conn' + synchronize do + unless @condition.wait_for(timeout) { @opened || @failed } + raise Timeout + end + end + if @failed + raise ConnectionFailed.new(@close_code) + end + end + + def run + # XXX: we don't really have a good way to exit this loop without + # getting the other end to kill the socket + loop do + begin + seg = read_segment + rescue Qpid::Closed => e + detach_all + break + end + @delegate.received(seg) + end + end + + def close(timeout=nil) + return unless @opened + Channel.new(self, 0).connection_close(200) + synchronize do + unless @condition.wait_for(timeout) { ! @opened } + raise Timeout + end + end + @thread.join(timeout) + @thread = nil + end + + def signal + synchronize { @condition.signal } + end + + def to_s + # FIXME: We'd like to report something like HOST:PORT + return @sock.to_s + end + + class Channel < Invoker + + attr_reader :id, :connection + attr_accessor :session + + def initialize(connection, id) + @connection = connection + @id = id + @session = nil + end + + def resolve_method(name) + inst = @connection.spec[name] + if inst.is_a?(Qpid::Spec010::Control) + return invocation(:method, inst) + else + return invocation(:error, nil) + end + end + + def invoke(type, args) + ctl = type.create(*args) + sc = StringCodec.new(@connection.spec) + sc.write_control(ctl) + @connection.write_segment(Segment.new(true, true, type.segment_type, + type.track, self.id, sc.encoded)) + + log = Qpid::logger["qpid.io.ctl"] + log.debug("SENT %s", ctl) if log + end + + def to_s + return "#{@connection}[#{@id}]" + end + + end + + end + +end diff --git a/qpid/ruby/lib/qpid/connection08.rb b/qpid/ruby/lib/qpid/connection08.rb new file mode 100644 index 0000000000..09a4888cc4 --- /dev/null +++ b/qpid/ruby/lib/qpid/connection08.rb @@ -0,0 +1,252 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "qpid/codec08" + +module Qpid08 + + class Connection + + def initialize(host, port, spec) + @host = host + @port = port + @spec = spec + end + + attr_reader(:host, :port, :spec) + + def connect() + @sock = TCPSocket.open(@host, @port) + @out = Encoder.new(@sock) + @in = Decoder.new(@sock) + end + + def init() + @out.write("AMQP") + [1, 1, @spec.major, @spec.minor].each {|o| + @out.octet(o) + } + end + + def write(frame) + # puts "OUT #{frame.inspect()}" + @out.octet(@spec.constants[frame.payload.type].id) + @out.short(frame.channel) + frame.payload.encode(@out) + @out.octet(frame_end) + end + + def read() + type = @spec.constants[@in.octet()].name + channel = @in.short() + payload = Payload.decode(type, @spec, @in) + oct = @in.octet() + if oct != frame_end + raise Exception.new("framing error: expected #{frame_end}, got #{oct}") + end + frame = Frame.new(channel, payload) + # puts " IN #{frame.inspect}" + return frame + end + + private + + def frame_end + @spec.constants[:"frame_end"].id + end + + end + + class Frame + + def initialize(channel, payload) + @channel = channel + @payload = payload + end + + attr_reader(:channel, :payload) + + end + + class Payload + + TYPES = {} + + def Payload.singleton_method_added(name) + if name == :type + TYPES[type] = self + end + end + + def Payload.decode(type, spec, dec) + klass = TYPES[type] + klass.decode(spec, dec) + end + + end + + class Method < Payload + + def initialize(method, args) + if args.size != method.fields.size + raise ArgumentError.new("argument mismatch #{method} #{args}") + end + @method = method + @args = args + end + + attr_reader(:method, :args) + + def Method.type; :frame_method end + + def type; Method.type end + + def encode(encoder) + buf = StringWriter.new() + enc = Encoder.new(buf) + enc.short(@method.parent.id) + enc.short(@method.id) + @method.fields.zip(self.args).each {|f, a| + if a.nil?; a = f.default end + enc.encode(f.type, a) + } + enc.flush() + encoder.longstr(buf.to_s) + end + + def Method.decode(spec, decoder) + buf = decoder.longstr() + dec = Decoder.new(StringReader.new(buf)) + klass = spec.classes[dec.short()] + meth = klass.methods[dec.short()] + args = meth.fields.map {|f| dec.decode(f.type)} + return Method.new(meth, args) + end + + def inspect(); "#{method.qname}(#{args.join(", ")})" end + + end + + class Header < Payload + + def Header.type; :frame_header end + + def initialize(klass, weight, size, properties) + @klass = klass + @weight = weight + @size = size + @properties = properties + end + + attr_reader :weight, :size, :properties + + def type; Header.type end + + def encode(encoder) + buf = StringWriter.new() + enc = Encoder.new(buf) + enc.short(@klass.id) + enc.short(@weight) + enc.longlong(@size) + + # property flags + nprops = @klass.fields.size + flags = 0 + 0.upto(nprops - 1) do |i| + f = @klass.fields[i] + flags <<= 1 + flags |= 1 unless @properties[f.name].nil? + # the last bit indicates more flags + if i > 0 and (i % 15) == 0 + flags <<= 1 + if nprops > (i + 1) + flags |= 1 + enc.short(flags) + flags = 0 + end + end + end + flags <<= ((16 - (nprops % 15)) % 16) + enc.short(flags) + + # properties + @klass.fields.each do |f| + v = @properties[f.name] + enc.encode(f.type, v) unless v.nil? + end + enc.flush() + encoder.longstr(buf.to_s) + end + + def Header.decode(spec, decoder) + dec = Decoder.new(StringReader.new(decoder.longstr())) + klass = spec.classes[dec.short()] + weight = dec.short() + size = dec.longlong() + + # property flags + bits = [] + while true + flags = dec.short() + 15.downto(1) do |i| + if flags >> i & 0x1 != 0 + bits << true + else + bits << false + end + end + break if flags & 0x1 == 0 + end + + # properties + properties = {} + bits.zip(klass.fields).each do |b, f| + properties[f.name] = dec.decode(f.type) if b + end + return Header.new(klass, weight, size, properties) + end + + def inspect(); "#{@klass.name}(#{@properties.inspect()})" end + + end + + class Body < Payload + + def Body.type; :frame_body end + + def type; Body.type end + + def initialize(content) + @content = content + end + + attr_reader :content + + def encode(enc) + enc.longstr(@content) + end + + def Body.decode(spec, dec) + return Body.new(dec.longstr()) + end + + end + +end diff --git a/qpid/ruby/lib/qpid/datatypes.rb b/qpid/ruby/lib/qpid/datatypes.rb new file mode 100644 index 0000000000..418388c73a --- /dev/null +++ b/qpid/ruby/lib/qpid/datatypes.rb @@ -0,0 +1,353 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 Qpid + + def self.struct(type, *args) + # FIXME: This is fragile; the last arg could be a hash, + # without being hte keywords + kwargs = {} + kwargs = args.pop if args.any? && args[-1].is_a?(Hash) + + if args.size > type.fields.size + raise TypeError, + "%s() takes at most %d arguments (%d given)" % + [type.name, type.fields.size, args.size] + end + + attrs = type.fields.inject({}) do |attrs, field| + if args.any? + attrs[field.name] = args.shift + if kwargs.key?(field.name) + raise TypeError, + "%s() got multiple values for keyword argument '%s'" % + [type.name, field.name] + end + elsif kwargs.key?(field.name) + attrs[field.name] = kwargs.delete(field.name) + else + attrs[field.name] = field.default + end + attrs + end + + unless kwargs.empty? + unexpected = kwargs.keys[0] + raise TypeError, + "%s() got an unexpected keyword argument '%s'" % + [type.name, unexpected] + end + + attrs[:st_type] = type + attrs[:id] = nil + + name = "Qpid_" + type.name.to_s.capitalize + unless ::Struct.const_defined?(name) + vars = type.fields.collect { |f| f.name } << :st_type << :id + ::Struct.new(name, *vars) + end + st = ::Struct.const_get(name) + + result = st.new + attrs.each { |k, v| result[k] = v } + return result + end + + class Message + + attr_accessor :headers, :body, :id + + def initialize(*args) + @body = nil + @headers = nil + + @body = args.pop unless args.empty? + @headers = args unless args.empty? + + @id = nil + end + + def has(name) + return ! get(name).nil? + end + + def get(name) + if @headers + name = name.to_sym + @headers.find { |h| h.st_type.name == name } + end + end + + def set(header) + @headers ||= [] + if h = @headers.find { |h| h.st_type == header.st_type } + ind = @headers.index(h) + @headers[ind] = header + else + @headers << header + end + end + + def clear(name) + if @headers + name = name.to_sym + @headers.delete_if { |h| h.st_type.name == name } + end + end + + # FIXME: Not sure what to do here + # Ruby doesn't have a notion of a evaluable string representation + # def __repr__(self): + # args = [] + # if self.headers: + # args.extend(map(repr, self.headers)) + # if self.body: + # args.append(repr(self.body)) + # if self.id is not None: + # args.append("id=%s" % self.id) + # return "Message(%s)" % ", ".join(args) + # end + end + + class ::Object + + def to_serial + Qpid::Serial.new(self) + end + end + + class Serial + + include Comparable + + attr_accessor :value + + def initialize(value) + @value = value & 0xFFFFFFFF + end + + def hash + @value.hash + end + + def to_serial + self + end + + def eql?(other) + other = other.to_serial + value.eql?(other.value) + end + + def <=>(other) + return 1 if other.nil? + + other = other.to_serial + + delta = (value - other.value) & 0xFFFFFFFF + neg = delta & 0x80000000 + mag = delta & 0x7FFFFFFF + + return (neg>0) ? -mag : mag + end + + def +(other) + result = other.to_serial + result.value += value + return result + end + + def -(other) + result = other.to_serial + result.value = value - result.value + return result + end + + def succ + Serial.new(value + 1) + end + + # FIXME: Not sure what to do here + # Ruby doesn't have a notion of a evaluable string representation + # def __repr__(self): + # return "serial(%s)" % self.value + # end + + def to_s + value.to_s + end + + end + + # The Python class datatypes.Range is emulated by the standard + # Range class with a few additions + class ::Range + + alias :lower :begin + alias :upper :end + + def touches(r) + # XXX: are we doing more checks than we need? + return (r.include?(lower - 1) || + r.include?(upper + 1) || + include?(r.lower - 1) || + include?(r.upper + 1) || + r.include?(lower) || + r.include?(upper) || + include?(r.lower) || + include?(r.upper)) + end + + def span(r) + Range.new([lower, r.lower].min, [upper, r.upper].max) + end + + def intersect(r) + l = [lower, r.lower].max + u = [upper, r.upper].min + return l > u ? nil : Range.new(l, u) + end + + end + + class RangedSet + + include Enumerable + + attr_accessor :ranges + + def initialize(*args) + @ranges = [] + args.each { |n| add(n) } + end + + def each(&block) + ranges.each { |r| yield(r) } + end + + def include?(n) + if (n.is_a?(Range)) + super(n) + else + ranges.find { |r| r.include?(n) } + end + end + + def add_range(range) + ranges.delete_if do |r| + if range.touches(r) + range = range.span(r) + true + else + false + end + end + ranges << range + end + + def add(lower, upper = nil) + upper = lower if upper.nil? + add_range(Range.new(lower, upper)) + end + + def to_s + repr = ranges.sort { |a,b| b.lower <=> a.lower }. + map { |r| r.to_s }.join(",") + "<RangedSet: {#{repr}}" + end + end + + class Future + def initialize(initial=nil, exception=Exception) + @value = initial + @error = nil + @set = Util::Event.new + @exception = exception + end + + def error(error) + @error = error + @set.set + end + + def set(value) + @value = value + @set.set + end + + def get(timeout=nil) + @set.wait(timeout) + unless @error.nil? + raise @exception.new(@error) + end + @value + end + end + + class UUID + include Comparable + + attr_accessor :bytes + + def initialize(bytes) + @bytes = bytes + end + + def <=>(other) + if other.respond_to?(:bytes) + return bytes <=> other.bytes + else + raise NotImplementedError + end + end + + def to_s + UUID::format(bytes) + end + + # FIXME: Not sure what to do here + # Ruby doesn't have a notion of a evaluable string representation + # def __repr__(self): + # return "UUID(%r)" % str(self) + # end + + def self.random_uuid + bytes = (1..16).collect { |i| rand(256) } + + # From RFC4122, the version bits are set to 0100 + bytes[7] &= 0x0F + bytes[7] |= 0x40 + + # From RFC4122, the top two bits of byte 8 get set to 01 + bytes[8] &= 0x3F + bytes[8] |= 0x80 + return bytes.pack("C16") + end + + def self.uuid4 + UUID.new(random_uuid) + end + + def self.format(s) + # Python format !LHHHHL + # big-endian, ulong, ushort x 4, ulong + "%08x-%04x-%04x-%04x-%04x%08x" % s.unpack("NnnnnN") + end + end +end diff --git a/qpid/ruby/lib/qpid/delegates.rb b/qpid/ruby/lib/qpid/delegates.rb new file mode 100644 index 0000000000..f779047e05 --- /dev/null +++ b/qpid/ruby/lib/qpid/delegates.rb @@ -0,0 +1,237 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 'rbconfig' +require 'sasl' + +module Qpid + + class Delegate + + def initialize(connection, args={}) + @connection = connection + @spec = connection.spec + @delegate = args[:delegate] || Qpid::Delegate::Client.method(:new) + @control = @spec[:track].enum[:control].value + end + + def log ; Qpid::logger["qpid.io.ctl"]; end + + def received(seg) + ssn = @connection.attached[seg.channel] + unless ssn + ch = Qpid::Connection::Channel.new(@connection, seg.channel) + else + ch = ssn.channel + end + + if seg.track == @control + ctl = seg.decode(@spec) + log.debug("RECV %s", ctl) if log + attr = ctl.st_type.name + method(attr).call(ch, ctl) + elsif ssn.nil? + ch.session_detached + else + ssn.received(seg) + end + end + + def connection_close(ch, close) + @connection.close_code = [close.reply_code, close.reply_text] + ch.connection_close_ok + @connection.sock.close_write() + unless @connection.opened + @connection.failed = true + @connection.signal + end + end + + def connection_close_ok(ch, close_ok) + @connection.opened = false + @connection.signal + end + + def session_attach(ch, a) + begin + @connection.attach(a.name, ch, @delegate, a.force) + ch.session_attached(a.name) + rescue Qpid::ChannelBusy + ch.session_detached(a.name) + rescue Qpid::SessionBusy + ch.session_detached(a.name) + end + end + + def session_attached(ch, a) + ch.session.signal + end + + def session_detach(ch, d) + #send back the confirmation of detachment before removing the + #channel from the attached set; this avoids needing to hold the + #connection lock during the sending of this control and ensures + #that if the channel is immediately reused for a new session the + #attach request will follow the detached notification. + ch.session_detached(d.name) + ssn = @connection.detach(d.name, ch) + end + + def session_detached(ch, d) + @connection.detach(d.name, ch) + end + + def session_request_timeout(ch, rt) + ch.session_timeout(rt.timeout) + end + + def session_command_point(ch, cp) + ssn = ch.session + ssn.receiver.next_id = cp.command_id + ssn.receiver.next_offset = cp.command_offset + end + + def session_completed(ch, cmp) + ch.session.sender.has_completed(cmp.commands) + if cmp.timely_reply + ch.session_known_completed(cmp.commands) + end + ch.session.signal + end + + def session_known_completed(ch, kn_cmp) + ch.session.receiver.known_completed(kn_cmp.commands) + end + + def session_flush(ch, f) + rcv = ch.session.receiver + if f.expected + if rcv.next_id + exp = Qpid::RangedSet.new(rcv.next_id) + else + exp = nil + end + ch.session_expected(exp) + end + if f.confirmed + ch.session_confirmed(rcv.completed) + end + if f.completed + ch.session_completed(rcv.completed) + end + end + + class Server < Delegate + + def start + @connection.read_header() + @connection.write_header(@spec.major, @spec.minor) + ch = Qpid::Connection::Channel.new(@connection, 0) + ch.connection_start(:mechanisms => ["ANONYMOUS"]) + ch + end + + def connection_start_ok(ch, start_ok) + ch.connection_tune(:channel_max => 65535) + end + + def connection_tune_ok(ch, tune_ok) + nil + end + + def connection_open(ch, open) + @connection.opened = true + ch.connection_open_ok() + @connection.signal + end + end + + class Client < Delegate + + # FIXME: Python uses os.name for platform - we don't have an exact + # analog in Ruby + PROPERTIES = {"product" => "qpid python client", + "version" => "development", + "platform" => Config::CONFIG["build_os"], + "qpid.client_process" => File.basename($0), + "qpid.client_pid" => Process.pid, + "qpid.client_ppid" => Process.ppid} + + + def initialize(connection, args) + super(connection) + + result = Sasl::client_init + + @mechanism= args[:mechanism] + @username = args[:username] + @password = args[:password] + @service = args[:service] || "qpidd" + @min_ssf = args[:min_ssf] || 0 + @max_ssf = args[:max_ssf] || 65535 + + @saslConn = Sasl.client_new(@mechanism, @service, args[:host], + @username, @password, @min_ssf, @max_ssf) + end + + def start + @connection.write_header(@spec.major, @spec.minor) + @connection.read_header + end + + def connection_start(ch, start) + mech_list = "" + start.mechanisms.each do |m| + mech_list += m + " " + end + begin + resp = Sasl.client_start(@saslConn, mech_list) + @connection.user_id = Sasl.user_id(@saslConn) + ch.connection_start_ok(:client_properties => PROPERTIES, + :mechanism => resp[2], + :response => resp[1]) + rescue exception + ch.connection_close(:message => $!.message) + @connection.failed = true + @connection.signal + end + end + + def connection_secure(ch, secure) + resp = Sasl.client_step(@saslConn, secure.challenge) + @connection.user_id = Sasl.user_id(@saslConn) + ch.connection_secure_ok(:response => resp[1]) + end + + def connection_tune(ch, tune) + ch.connection_tune_ok(:channel_max => tune.channel_max, + :max_frame_size => tune.max_frame_size, + :heartbeat => 0) + ch.connection_open() + @connection.security_layer_tx = @saslConn + end + + def connection_open_ok(ch, open_ok) + @connection.security_layer_rx = @saslConn + @connection.opened = true + @connection.signal + end + end + end +end diff --git a/qpid/ruby/lib/qpid/fields.rb b/qpid/ruby/lib/qpid/fields.rb new file mode 100644 index 0000000000..cc87d07529 --- /dev/null +++ b/qpid/ruby/lib/qpid/fields.rb @@ -0,0 +1,49 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +class Class + def fields(*fields) + module_eval { + def initialize(*args, &block) + args = init_fields(*args) + + if respond_to? :init + init(*args) {|*a| yield(*a)} + elsif args.any? + raise ArgumentError, "extra arguments: #{args.inspect}" + end + end + } + + vars = fields.map {|f| :"@#{f.to_s().chomp("?")}"} + + define_method(:init_fields) {|*args| + vars.each {|v| + instance_variable_set(v, args.shift()) + } + args + } + + vars.each_index {|i| + define_method(fields[i]) { + instance_variable_get(vars[i]) + } + } + end +end diff --git a/qpid/ruby/lib/qpid/framer.rb b/qpid/ruby/lib/qpid/framer.rb new file mode 100644 index 0000000000..d057605383 --- /dev/null +++ b/qpid/ruby/lib/qpid/framer.rb @@ -0,0 +1,212 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 'monitor' +require 'logger' +require 'sasl' + +module Qpid + + FIRST_SEG = 0x08 + LAST_SEG = 0x04 + FIRST_FRM = 0x02 + LAST_FRM = 0x01 + + class << self + attr_accessor :raw_logger, :frm_logger + end + + def self.packed_size(format) + # FIXME: This is a total copout to simulate Python's + # struct.calcsize + ([0]*256).pack(format).size + end + + class Frame + attr_reader :payload, :track, :flags, :type, :channel + + # HEADER = "!2BHxBH4x" + # Python Meaning Ruby + # ! big endian (implied by format char) + # 2B 2 uchar C2 + # H unsigned short n + # x pad byte x + # B uchar C + # H unsigned short n + # 4x pad byte x4 + HEADER = "C2nxCnx4" + HEADER_SIZE = Qpid::packed_size(HEADER) + MAX_PAYLOAD = 65535 - HEADER_SIZE + + def initialize(flags, type, track, channel, payload) + if payload.size > MAX_PAYLOAD + raise ArgumentError, "max payload size exceeded: #{payload.size}" + end + + @flags = flags + @type = type + @track = track + @channel = channel + @payload = payload + end + + def first_segment? ; FIRST_SEG & @flags > 0 ; end + + def last_segment? ; LAST_SEG & @flags > 0 ; end + + def first_frame? ; FIRST_FRM & @flags > 0 ; end + + def last_frame? ; LAST_FRM & @flags > 0 ; end + + def to_s + fs = first_segment? ? 'S' : '.' + ls = last_segment? ? 's' : '.' + ff = first_frame? ? 'F' : '.' + lf = last_frame? ? 'f' : '.' + + return "%s%s%s%s %s %s %s %s" % [fs, ls, ff, lf, + @type, + @track, + @channel, + @payload.inspect] + end + end + + class FramingError < Exception ; end + + class Closed < Exception ; end + + class Framer + include Packer + + # Python: "!4s4B" + HEADER = "a4C4" + HEADER_SIZE = 8 + + def raw + Qpid::raw_logger + end + + def frm + Qpid::frm_logger + end + + def initialize(sock) + @sock = sock + @sock.extend(MonitorMixin) + @tx_buf = "" + @rx_buf = "" + @security_layer_tx = nil + @security_layer_rx = nil + @maxbufsize = 65535 + end + + attr_reader :sock + attr_accessor :security_layer_tx, :security_layer_rx + + def aborted? ; false ; end + + def write(buf) + @tx_buf += buf + end + + def flush + @sock.synchronize do + if @security_layer_tx + cipher_buf = Sasl.encode(@security_layer_tx, @tx_buf) + _write(cipher_buf) + else + _write(@tx_buf) + end + @tx_buf = "" + frm.debug("FLUSHED") if frm + end + rescue + @sock.close unless @sock.closed? + end + + def _write(buf) + while buf && buf.size > 0 + # FIXME: Catch errors + n = @sock.write(buf) + raw.debug("SENT #{buf[0, n].inspect}") if raw + buf[0,n] = "" + @sock.flush + end + end + + def read(n) + while @rx_buf.size < n + begin + s = @sock.recv(@maxbufsize) + if @security_layer_rx + s = Sasl.decode(@security_layer_rx, s) + end + rescue IOError => e + raise e if @rx_buf != "" + @sock.close unless @sock.closed? + raise Closed + end + # FIXME: Catch errors + if s.nil? or s.size == 0 + @sock.close unless @sock.closed? + raise Closed + end + @rx_buf += s + raw.debug("RECV #{n}/#{@rx_buf.size} #{s.inspect}") if raw + end + data = @rx_buf[0, n] + @rx_buf = @rx_buf[n, @rx_buf.size - n] + return data + end + + def read_header + unpack(Framer::HEADER, Framer::HEADER_SIZE) + end + + def write_header(major, minor) + @sock.synchronize do + pack(Framer::HEADER, "AMQP", 1, 1, major, minor) + flush() + end + end + + def write_frame(frame) + @sock.synchronize do + size = frame.payload.size + Frame::HEADER_SIZE + track = frame.track & 0x0F + pack(Frame::HEADER, frame.flags, frame.type, size, track, frame.channel) + write(frame.payload) + if frame.last_segment? and frame.last_frame? + flush() + frm.debug("SENT #{frame}") if frm + end + end + end + + def read_frame + flags, type, size, track, channel = unpack(Frame::HEADER, Frame::HEADER_SIZE) + raise FramingError if (flags & 0xF0 > 0) + payload = read(size - Frame::HEADER_SIZE) + frame = Frame.new(flags, type, track, channel, payload) + frm.debug("RECV #{frame}") if frm + return frame + end + end +end diff --git a/qpid/ruby/lib/qpid/invoker.rb b/qpid/ruby/lib/qpid/invoker.rb new file mode 100644 index 0000000000..39716ac6c2 --- /dev/null +++ b/qpid/ruby/lib/qpid/invoker.rb @@ -0,0 +1,65 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +class Qpid::Invoker + + # Requires that client defines a invoke method and overrides + # resolve_method + + # FIXME: Is it really worth defining methods in method_missing ? We + # could just dispatch there directly + + def invc_method(name, resolved) + define_singleton_method(name) { |*args| invoke(resolved, args) } + # FIXME: the Python code also attaches docs from resolved.pydoc + end + + def invc_value(name, resolved) + define_singleton_method(name) { | | resolved } + end + + def invc_error(name, resolved) + msg = "%s instance has no attribute '%s'" % [self.class.name, name] + if resolved + msg += "\n%s" % resolved + end + raise NameError, msg + end + + def resolve_method(name) + invocation(:error, nil) + end + + def method_missing(name, *args) + disp, resolved = resolve_method(name) + disp.call(name, resolved) + send(name, *args) + end + + def invocation(kind, name = nil) + [ method("invc_#{kind}"), name ] + end + + private + def define_singleton_method(name, &body) + singleton_class = class << self; self; end + singleton_class.send(:define_method, name, &body) + end + +end diff --git a/qpid/ruby/lib/qpid/packer.rb b/qpid/ruby/lib/qpid/packer.rb new file mode 100644 index 0000000000..ae1be37faf --- /dev/null +++ b/qpid/ruby/lib/qpid/packer.rb @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 Qpid + module Packer + def unpack(fmt, len) + raw = read(len) + values = raw.unpack(fmt) + values = values[0] if values.size == 1 + return values + end + + def pack(fmt, *args) + write(args.pack(fmt)) + end + end +end diff --git a/qpid/ruby/lib/qpid/peer.rb b/qpid/ruby/lib/qpid/peer.rb new file mode 100644 index 0000000000..cdb962169b --- /dev/null +++ b/qpid/ruby/lib/qpid/peer.rb @@ -0,0 +1,289 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "thread" +require "qpid/queue" +require "qpid/connection08" +require "qpid/fields" + +module Qpid08 + + Queue = Qpid::Queue + + class Peer + + def initialize(conn, delegate) + @conn = conn + @delegate = delegate + @outgoing = Queue.new() + @work = Queue.new() + @channels = {} + @mutex = Mutex.new() + end + + def channel(id) + @mutex.synchronize do + ch = @channels[id] + if ch.nil? + ch = Channel.new(id, self, @outgoing, @conn.spec) + @channels[id] = ch + end + return ch + end + end + + def channel_delete(id) + @channels.delete(id) + end + + def start() + spawn(:writer) + spawn(:reader) + spawn(:worker) + end + + def close() + @mutex.synchronize do + @channels.each_value do |ch| + ch.close() + end + @outgoing.close() + @work.close() + end + end + + private + + def spawn(method, *args) + Thread.new do + begin + send(method, *args) + # is this the standard way to catch any exception? + rescue Closed => e + puts "#{method} #{e}" + rescue Object => e + print e + e.backtrace.each do |line| + print "\n ", line + end + print "\n" + end + end + end + + def reader() + while true + frame = @conn.read() + ch = channel(frame.channel) + ch.dispatch(frame, @work) + end + end + + def writer() + while true + @conn.write(@outgoing.get()) + end + end + + def worker() + while true + dispatch(@work.get()) + end + end + + def dispatch(queue) + frame = queue.get() + ch = channel(frame.channel) + payload = frame.payload + if payload.method.content? + content = Qpid08::read_content(queue) + else + content = nil + end + + message = Message.new(payload.method, payload.args, content) + @delegate.dispatch(ch, message) + end + + end + + class Channel + def initialize(id, peer, outgoing, spec) + @id = id + @peer = peer + @outgoing = outgoing + @spec = spec + @incoming = Queue.new() + @responses = Queue.new() + @queue = nil + @closed = false + end + + attr_reader :id + + def closed?; @closed end + + def close() + return if closed? + @peer.channel_delete(@id) + @closed = true + @incoming.close() + @responses.close() + end + + def dispatch(frame, work) + payload = frame.payload + case payload + when Method + if payload.method.response? + @queue = @responses + else + @queue = @incoming + work << @incoming + end + end + @queue << frame + end + + def method_missing(name, *args) + method = @spec.find_method(name) + if method.nil? + raise NoMethodError.new("undefined method '#{name}' for #{self}:#{self.class}") + end + + if args.size == 1 and args[0].instance_of? Hash + kwargs = args[0] + invoke_args = method.fields.map do |f| + kwargs[f.name] + end + content = kwargs[:content] + else + invoke_args = [] + method.fields.each do |f| + if args.any? + invoke_args << args.shift() + else + invoke_args << f.default + end + end + if method.content? and args.any? + content = args.shift() + else + content = nil + end + if args.any? then raise ArgumentError.new("#{args.size} extr arguments") end + end + return invoke(method, invoke_args, content) + end + + def invoke(method, args, content = nil) + raise Closed() if closed? + frame = Frame.new(@id, Method.new(method, args)) + @outgoing << frame + + if method.content? + content = Content.new() if content.nil? + write_content(method.parent, content, @outgoing) + end + + nowait = false + f = method.fields[:"nowait"] + nowait = args[method.fields.index(f)] unless f.nil? + + unless nowait or method.responses.empty? + resp = @responses.get().payload + if resp.method.content? + content = read_content(@responses) + else + content = nil + end + if method.responses.include? resp.method + return Message.new(resp.method, resp.args, content) + else + # XXX: ValueError doesn't actually exist + raise ValueError.new(resp) + end + end + end + + def write_content(klass, content, queue) + size = content.size + header = Frame.new(@id, Header.new(klass, content.weight, size, content.headers)) + queue << header + content.children.each {|child| write_content(klass, child, queue)} + queue << Frame.new(@id, Body.new(content.body)) if size > 0 + end + + end + + def Qpid08.read_content(queue) + frame = queue.get() + header = frame.payload + children = [] + 1.upto(header.weight) { children << read_content(queue) } + size = header.size + read = 0 + buf = "" + while read < size + body = queue.get() + content = body.payload.content + buf << content + read += content.size + end + buf.freeze() + return Content.new(header.properties.clone(), buf, children) + end + + class Content + def initialize(headers = {}, body = "", children = []) + @headers = headers + @body = body + @children = children + end + + attr_reader :headers, :body, :children + + def size; body.size end + def weight; children.size end + + def [](key); @headers[key] end + def []=(key, value); @headers[key] = value end + end + + class Message + fields(:method, :args, :content) + + alias fields args + + def method_missing(name) + return args[@method.fields[name].id] + end + + def inspect() + "#{method.qname}(#{args.join(", ")})" + end + end + + module Delegate + def dispatch(ch, msg) + send(msg.method.qname, ch, msg) + end + end + +end diff --git a/qpid/ruby/lib/qpid/qmf.rb b/qpid/ruby/lib/qpid/qmf.rb new file mode 100644 index 0000000000..4711d355cd --- /dev/null +++ b/qpid/ruby/lib/qpid/qmf.rb @@ -0,0 +1,1957 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +# Console API for Qpid Management Framework + +require 'socket' +require 'monitor' +require 'thread' +require 'uri' +require 'time' + +module Qpid::Qmf + + # To access the asynchronous operations, a class must be derived from + # Console with overrides of any combination of the available methods. + class Console + + # Invoked when a connection is established to a broker + def broker_connected(broker); end + + # Invoked when the connection to a broker is lost + def broker_disconnected(broker); end + + # Invoked when a QMF package is discovered + def new_package(name); end + + # Invoked when a new class is discovered. Session.getSchema can be + # used to obtain details about the class + def new_class(kind, klass_key); end + + # Invoked when a QMF agent is discovered + def new_agent(agent); end + + # Invoked when a QMF agent disconects + def del_agent(agent); end + + # Invoked when an object is updated + def object_props(broker, record); end + + # Invoked when an object is updated + def object_stats(broker, record); end + + # Invoked when an event is raised + def event(broker, event); end + + # Invoked when an agent heartbeat is received. + def heartbeat(agent, timestamp); end + + # Invoked when the connection sequence reaches the point where broker information is available. + def broker_info(broker); end + + # Invoked when a method response from an asynchronous method call is received. + def method_response(broker, seq, response); end + end + + class BrokerURL + + attr_reader :host, :port, :auth_name, :auth_pass + + def initialize(text) + uri = URI.parse(text) + + @host = uri.host + @port = uri.port ? uri.port : 5672 + @auth_name = uri.user + @auth_pass = uri.password + + return uri + end + + def name + "#{@host}:#{@port}" + end + + def match(host, port) + # FIXME: Unlcear what the Python code is actually checking for + # here, especially since HOST can resolve to multiple IP's + @port == port && + (host == @host || ipaddr(host, port) == ipaddr(@host, @port)) + end + + private + def ipaddr(host, port) + s = Socket::getaddrinfo(host, port, + Socket::AF_INET, Socket::SOCK_STREAM) + s[0][2] + end + end + + # An instance of the Session class represents a console session running + # against one or more QMF brokers. A single instance of Session is + # needed to interact with the management framework as a console. + class Session + CONTEXT_SYNC = 1 + CONTEXT_STARTUP = 2 + CONTEXT_MULTIGET = 3 + + DEFAULT_GET_WAIT_TIME = 60 + + include MonitorMixin + + attr_reader :binding_key_list, :select, :seq_mgr, :console, :packages + + # Initialize a session. If the console argument is provided, the + # more advanced asynchronous features are available. If console is + # defaulted, the session will operate in a simpler, synchronous + # manner. The rcvObjects, rcvEvents, and rcvHeartbeats arguments + # are meaningful only if 'console' is provided. They control + # whether object updates, events, and agent-heartbeats are + # subscribed to. If the console is not interested in receiving one + # or more of the above, setting the argument to False will reduce + # tha bandwidth used by the API. If manageConnections is set to + # True, the Session object will manage connections to the brokers. + # This means that if a broker is unreachable, it will retry until a + # connection can be established. If a connection is lost, the + # Session will attempt to reconnect. + # + # If manageConnections is set to False, the user is responsible for + # handing failures. In this case, an unreachable broker will cause + # addBroker to raise an exception. If userBindings is set to False + # (the default) and rcvObjects is True, the console will receive + # data for all object classes. If userBindings is set to True, the + # user must select which classes the console shall receive by + # invoking the bindPackage or bindClass methods. This allows the + # console to be configured to receive only information that is + # relavant to a particular application. If rcvObjects id False, + # userBindings has no meaning. + # + # Accept a hash of parameters, where keys can be :console, + # :rcv_objects, :rcv_events, :rcv_heartbeats, :manage_connections, + # and :user_bindings + def initialize(kwargs = {}) + super() + @console = kwargs[:console] || nil + @brokers = [] + @packages = {} + @seq_mgr = SequenceManager.new + @cv = new_cond + @sync_sequence_list = [] + @result = [] + @select = [] + @error = nil + @rcv_objects = kwargs[:rcv_objects] == nil ? true : kwargs[:rcv_objects] + @rcv_events = kwargs[:rcv_events] == nil ? true : kwargs[:rcv_events] + @rcv_heartbeats = kwargs[:rcv_heartbeats] == nil ? true : kwargs[:rcv_heartbeats] + @user_bindings = kwargs[:user_bindings] == nil ? false : kwargs[:user_bindings] + unless @console + @rcv_objects = false + @rcv_events = false + @rcv_heartbeats = false + end + @binding_key_list = binding_keys + @manage_connections = kwargs[:manage_connections] || false + + if @user_bindings && ! @rcv_objects + raise ArgumentError, "user_bindings can't be set unless rcv_objects is set and a console is provided" + end + + end + + def to_s + "QMF Console Session Manager (brokers: #{@brokers.size})" + end + + def managedConnections? + return @manage_connections + end + + # Connect to a Qpid broker. Returns an object of type Broker + # + # To supply a username for authentication, use the URL syntax: + # + # amqp://username@hostname:port + # + # If the broker needs a password for the client, an interactive prompt will be + # provided to the user. + # + # To supply a username and a password, use + # + # amqp://username:password@hostname:port + # + # The following keyword arguments may be used to control authentication: + # + # :mechanism - SASL mechanism (i.e. "PLAIN", "GSSAPI", "ANONYMOUS", etc. + # - defaults to unspecified (the system chooses for you) + # :service - SASL service name (i.e. the kerberos principal of the broker) + # - defaults to "qpidd" + # :min_ssf - Minimum Security Strength Factor for SASL security layers + # - defaults to 0 + # :max_ssf - Maximum Security Strength Factor for SASL security layers + # - defaults to 65535 + # + def add_broker(target = "amqp://localhost", kwargs = {}) + url = BrokerURL.new(target) + broker = Broker.new(self, url.host, url.port, url.auth_name, url.auth_pass, kwargs) + unless broker.connected? || @manage_connections + raise broker.error + end + + @brokers << broker + objects(:broker => broker, :class => "agent") unless @manage_connections + return broker + end + + # Disconnect from a broker. The 'broker' argument is the object + # returned from the addBroker call + def del_broker(broker) + broker.shutdown + @brokers.delete(broker) + end + + # Get the list of known classes within a QMF package + def classes(package_name) + list = [] + @brokers.each { |broker| broker.wait_for_stable } + if @packages.include?(package_name) + # FIXME What's the actual structure of @packages[package_name] + @packages[package_name].each do |key, schema_class| + list << schema_class.klass_key + end + end + return list + end + + # Get the schema for a QMF class + def schema(klass_key) + @brokers.each { |broker| broker.wait_for_stable } + if @packages.include?(klass_key.package) + @packages[klass_key.package][ [klass_key.klass_name, klass_key.hash] ] + end + end + + def bind_package(package_name) + unless @user_bindings && @rcv_objects + raise "userBindings option not set for Session" + end + @brokers.each do |broker| + args = { :exchange => "qpid.management", + :queue => broker.topic_name, + :binding_key => "console.obj.*.*.#{package_name}.#" } + broker.amqp_session.exchange_bind(args) + end + end + + def bind_class(package_name, class_name) + unless @user_bindings && @rcv_objects + raise "userBindings option not set for Session" + end + @brokers.each do |broker| + args = { :exchange => "qpid.management", + :queue => broker.topic_name, + :binding_key=> "console.obj.*.*.#{package_name}.#{class_name}.#" } + broker.amqp_session.exchange_bind(args) + end + end + + def bind_class_key(klass_key) + unless @user_bindings && @rcv_objects + raise "userBindings option not set for Session" + end + pname, cname, hash = klass_key.to_a() + @brokers.each do |broker| + args = { :exchange => "qpid.management", + :queue => broker.topic_name, + :binding_key => "console.obj.*.*.#{pname}.#{cname}.#" } + broker.amqp_session.exchange_bind(args) + end + end + + # Get a list of currently known agents + def agents(broker=nil) + broker_list = [] + if broker.nil? + broker_list = @brokers.dup + else + broker_list << broker + end + broker_list.each { |b| b.wait_for_stable } + agent_list = [] + broker_list.each { |b| agent_list += b.agents } + return agent_list + end + + # Get a list of objects from QMF agents. + # All arguments are passed by name(keyword). + # + # The class for queried objects may be specified in one of the + # following ways: + # :schema => <schema> - supply a schema object returned from getSchema. + # :key => <key> - supply a klass_key from the list returned by getClasses. + # :class => <name> - supply a class name as a string. If the class name exists + # in multiple packages, a _package argument may also be supplied. + # :object_id = <id> - get the object referenced by the object-id + # + # If objects should be obtained from only one agent, use the following argument. + # Otherwise, the query will go to all agents. + # + # :agent = <agent> - supply an agent from the list returned by getAgents. + # + # If the get query is to be restricted to one broker (as opposed to + # all connected brokers), add the following argument: + # + # :broker = <broker> - supply a broker as returned by addBroker. + # + # The default timeout for this synchronous operation is 60 seconds. To change the timeout, + # use the following argument: + # + # :timeout = <time in seconds> + # + # If additional arguments are supplied, they are used as property + # selectors, as long as their keys are strings. For example, if + # the argument "name" => "test" is supplied, only objects whose + # "name" property is "test" will be returned in the result. + def objects(kwargs) + if kwargs.include?(:broker) + broker_list = [] + broker_list << kwargs[:broker] + else + broker_list = @brokers + end + broker_list.each { |broker| + broker.wait_for_stable + if kwargs[:package] != "org.apache.qpid.broker" or kwargs[:class] != "agent" + objects(:agent => broker.agent(1,0), :package => "org.apache.qpid.broker", :class => "agent") if broker.connected? + end + } + + agent_list = [] + if kwargs.include?(:agent) + agent = kwargs[:agent] + unless broker_list.include?(agent.broker) + raise ArgumentError, "Supplied agent is not accessible through the supplied broker" + end + agent_list << agent if agent.broker.connected? + else + if kwargs.include?(:object_id) + oid = kwargs[:object_id] + broker_list.each { |broker| + broker.agents.each { |agent| + if oid.broker_bank == agent.broker_bank && oid.agent_bank == agent.agent_bank + agent_list << agent if agent.broker.connected? + end + } + } + else + broker_list.each { |broker| + agent_list += broker.agents if broker.connected? + } + end + end + + cname = nil + if kwargs.include?(:schema) + # FIXME: What kind of object is kwargs[:schema] + pname, cname, hash = kwargs[:schema].getKey().to_a + elsif kwargs.include?(:key) + pname, cname, hash = kwargs[:key].to_a + elsif kwargs.include?(:class) + pname, cname, hash = [kwargs[:package], kwargs[:class], nil] + end + if cname.nil? && ! kwargs.include?(:object_id) + raise ArgumentError, + "No class supplied, use :schema, :key, :class, or :object_id' argument" + end + + map = {} + @select = [] + if kwargs.include?(:object_id) + map["_objectid"] = kwargs[:object_id].to_s + else + map["_class"] = cname + map["_package"] = pname if pname + map["_hash"] = hash if hash + kwargs.each do |k,v| + @select << [k, v] if k.is_a?(String) + end + end + + @result = [] + agent_list.each do |agent| + broker = agent.broker + send_codec = Qpid::StringCodec.new(broker.conn.spec) + seq = nil + synchronize do + seq = @seq_mgr.reserve(CONTEXT_MULTIGET) + @sync_sequence_list << seq + end + broker.set_header(send_codec, ?G, seq) + send_codec.write_map(map) + bank_key = "%d.%d" % [broker.broker_bank, agent.agent_bank] + smsg = broker.message(send_codec.encoded, "agent.#{bank_key}") + broker.emit(smsg) + end + + timeout = false + if kwargs.include?(:timeout) + wait_time = kwargs[:timeout] + else + wait_time = DEFAULT_GET_WAIT_TIME + end + synchronize do + unless @cv.wait_for(wait_time) { @sync_sequence_list.empty? || @error } + @sync_sequence_list.each do |pending_seq| + @seq_mgr.release(pending_seq) + end + @sync_sequence_list = [] + timeout = true + end + end + + if @error + errorText = @error + @error = nil + raise errorText + end + + if @result.empty? && timeout + raise "No agent responded within timeout period" + end + @result + end + + # Return one and only one object or nil. + def object(kwargs) + objs = objects(kwargs) + return objs.length == 1 ? objs[0] : nil + end + + # Return the first of potentially many objects. + def first_object(kwargs) + objs = objects(kwargs) + return objs.length > 0 ? objs[0] : nil + end + + def set_event_filter(kwargs); end + + def handle_broker_connect(broker); end + + def handle_broker_resp(broker, codec, seq) + broker.broker_id = codec.read_uuid + @console.broker_info(broker) if @console + + # Send a package request + # (effectively inc and dec outstanding by not doing anything) + send_codec = Qpid::StringCodec.new(broker.conn.spec) + seq = @seq_mgr.reserve(CONTEXT_STARTUP) + broker.set_header(send_codec, ?P, seq) + smsg = broker.message(send_codec.encoded) + broker.emit(smsg) + end + + def handle_package_ind(broker, codec, seq) + pname = codec.read_str8 + new_package = false + synchronize do + new_package = ! @packages.include?(pname) + @packages[pname] = {} if new_package + end + @console.new_package(pname) if @console + + # Send a class request + broker.inc_outstanding + send_codec = Qpid::StringCodec.new(broker.conn.spec) + seq = @seq_mgr.reserve(CONTEXT_STARTUP) + broker.set_header(send_codec, ?Q, seq) + send_codec.write_str8(pname) + smsg = broker.message(send_codec.encoded) + broker.emit(smsg) + end + + def handle_command_complete(broker, codec, seq) + code = codec.read_uint32 + text = codec.read_str8 + context = @seq_mgr.release(seq) + if context == CONTEXT_STARTUP + broker.dec_outstanding + elsif context == CONTEXT_SYNC && seq == broker.sync_sequence + broker.sync_done + elsif context == CONTEXT_MULTIGET && @sync_sequence_list.include?(seq) + synchronize do + @sync_sequence_list.delete(seq) + @cv.signal if @sync_sequence_list.empty? + end + end + end + + def handle_class_ind(broker, codec, seq) + kind = codec.read_uint8 + classKey = ClassKey.new(codec) + unknown = false + + synchronize do + return unless @packages.include?(classKey.package) + unknown = true unless @packages[classKey.package].include?([classKey.klass_name, classKey.hash]) + end + + + if unknown + # Send a schema request for the unknown class + broker.inc_outstanding + send_codec = Qpid::StringCodec.new(broker.conn.spec) + seq = @seq_mgr.reserve(CONTEXT_STARTUP) + broker.set_header(send_codec, ?S, seq) + classKey.encode(send_codec) + smsg = broker.message(send_codec.encoded) + broker.emit(smsg) + end + end + + def handle_method_resp(broker, codec, seq) + code = codec.read_uint32 + text = codec.read_str16 + out_args = {} + pair = @seq_mgr.release(seq) + return unless pair + method, synchronous = pair + if code == 0 + method.arguments.each do |arg| + if arg.dir.index(?O) + out_args[arg.name] = decode_value(codec, arg.type) + end + end + end + result = MethodResult.new(code, text, out_args) + if synchronous: + broker.synchronize do + broker.sync_result = MethodResult.new(code, text, out_args) + broker.sync_done + end + else + @console.method_response(broker, seq, result) if @console + end + end + + def handle_heartbeat_ind(broker, codec, seq, msg) + if @console + broker_bank = 1 + agent_bank = 0 + dp = msg.get("delivery_properties") + if dp + key = dp["routing_key"] + key_elements = key.split(".") + if key_elements.length == 4 + broker_bank = key_elements[2].to_i + agent_bank = key_elements[3].to_i + end + end + agent = broker.agent(broker_bank, agent_bank) + timestamp = codec.read_uint64 + @console.heartbeat(agent, timestamp) if agent + end + end + + def handle_event_ind(broker, codec, seq) + if @console + event = Event.new(self, broker, codec) + @console.event(broker, event) + end + end + + def handle_schema_resp(broker, codec, seq) + kind = codec.read_uint8 + classKey = ClassKey.new(codec) + klass = SchemaClass.new(self, kind, classKey, codec) + synchronize { @packages[classKey.package][ [classKey.klass_name, classKey.hash] ] = klass } + + @seq_mgr.release(seq) + broker.dec_outstanding + @console.new_class(kind, classKey) if @console + end + + def handle_content_ind(broker, codec, seq, prop=false, stat=false) + klass_key = ClassKey.new(codec) + pname, cname, hash = klass_key.to_a() ; + + schema = nil + synchronize do + return unless @packages.include?(klass_key.package) + return unless @packages[klass_key.package].include?([klass_key.klass_name, klass_key.hash]) + schema = @packages[klass_key.package][ [klass_key.klass_name, klass_key.hash] ] + end + + + object = Qpid::Qmf::Object.new(self, broker, schema, codec, prop, stat) + if pname == "org.apache.qpid.broker" && cname == "agent" && prop + broker.update_agent(object) + end + + synchronize do + if @sync_sequence_list.include?(seq) + if object.timestamps()[2] == 0 && select_match(object) + @result << object + end + return + end + end + + @console.object_props(broker, object) if @console && @rcv_objects && prop + @console.object_stats(broker, object) if @console && @rcv_objects && stat + end + + def handle_broker_disconnect(broker); end + + def handle_error(error) + synchronize do + @error = error if @sync_sequence_list.length > 0 + @sync_sequence_list = [] + @cv.signal + end + end + + # Decode, from the codec, a value based on its typecode + def decode_value(codec, typecode) + case typecode + when 1: data = codec.read_uint8 # U8 + when 2: data = codec.read_uint16 # U16 + when 3: data = codec.read_uint32 # U32 + when 4: data = codec.read_uint64 # U64 + when 6: data = codec.read_str8 # SSTR + when 7: data = codec.read_str16 # LSTR + when 8: data = codec.read_int64 # ABSTIME + when 9: data = codec.read_uint64 # DELTATIME + when 10: data = ObjectId.new(codec) # REF + when 11: data = codec.read_uint8 != 0 # BOOL + when 12: data = codec.read_float # FLOAT + when 13: data = codec.read_double # DOUBLE + when 14: data = codec.read_uuid # UUID + when 15: data = codec.read_map # FTABLE + when 16: data = codec.read_int8 # S8 + when 17: data = codec.read_int16 # S16 + when 18: data = codec.read_int32 # S32 + when 19: data = codec.read_int64 # S64 + when 20: # Object + inner_type_code = codec.read_uint8() + if (inner_type_code == 20) + classKey = ClassKey.new(codec) + innerSchema = schema(classKey) + data = Object.new(self, @broker, innerSchema, codec, true, true, false) if innerSchema + else + data = decode_value(codec, inner_type_code) + end + when 21: + data = [] + rec_codec = Qpid::StringCodec.new(codec.spec, codec.read_vbin32()) + count = rec_codec.read_uint32() + while count > 0 do + type = rec_codec.read_uint8() + data << (decode_value(rec_codec,type)) + count -= 1 + end + when 22: + data = [] + rec_codec = Qpid::StringCodec.new(codec.spec, codec.read_vbin32()) + count = rec_codec.read_uint32() + type = rec_codec.read_uint8() + while count > 0 do + data << (decode_value(rec_codec,type)) + count -= 1 + end + else + raise ArgumentError, "Invalid type code: #{typecode} - #{typecode.inspect}" + end + return data + end + + ENCODINGS = { + String => 6, + Fixnum => 18, + Bignum => 19, + Float => 12, + Array => 21, + Hash => 15 + } + + def encoding(object) + klass = object.class + if ENCODINGS.has_key?(klass) + return ENCODINGS[klass] + end + for base in klass.__bases__ + result = encoding(base) + return result unless result.nil? + end + end + + # Encode, into the codec, a value based on its typecode + def encode_value(codec, value, typecode) + # FIXME: Python does a lot of magic type conversions + # We just assume that value has the right type; this is safer + # than coercing explicitly, since Array::pack will complain + # loudly about various type errors + case typecode + when 1: codec.write_uint8(value) # U8 + when 2: codec.write_uint16(value) # U16 + when 3: codec.write_uint32(value) # U32 + when 4: codec.write_uint64(value) # U64 + when 6: codec.write_str8(value) # SSTR + when 7: codec.write_str16(value) # LSTR + when 8: codec.write_int64(value) # ABSTIME + when 9: codec.write_uint64(value) # DELTATIME + when 10: value.encode(codec) # REF + when 11: codec.write_uint8(value ? 1 : 0) # BOOL + when 12: codec.write_float(value) # FLOAT + when 13: codec.write_double(value) # DOUBLE + when 14: codec.write_uuid(value) # UUID + when 15: codec.write_map(value) # FTABLE + when 16: codec.write_int8(value) # S8 + when 17: codec.write_int16(value) # S16 + when 18: codec.write_int32(value) # S32 + when 19: codec.write_int64(value) # S64 + when 20: value.encode(codec) + when 21: # List + send_codec = Qpid::StringCodec.new(codec.spec) + encode_value(send_codec, value.size, 3) + value.each do v + ltype = encoding(v) + encode_value(send_codec,ltype,1) + encode_value(send_codec,v,ltype) + end + codec.write_vbin32(send_codec.encoded) + when 22: # Array + send_codec = Qpid::StringCodec.new(codec.spec) + encode_value(send_codec, value.size, 3) + if value.size > 0 + ltype = encoding(value[0]) + encode_value(send_codec,ltype,1) + value.each do v + encode_value(send_codec,v,ltype) + end + end + codec.write_vbin32(send_codec.encoded) + else + raise ValueError, "Invalid type code: %d" % typecode + end + end + + def display_value(value, typecode) + case typecode + when 1: return value.to_s + when 2: return value.to_s + when 3: return value.to_s + when 4: return value.to_s + when 6: return value.to_s + when 7: return value.to_s + when 8: return strftime("%c", gmtime(value / 1000000000)) + when 9: return value.to_s + when 10: return value.to_s + when 11: return value ? 'T' : 'F' + when 12: return value.to_s + when 13: return value.to_s + when 14: return Qpid::UUID::format(value) + when 15: return value.to_s + when 16: return value.to_s + when 17: return value.to_s + when 18: return value.to_s + when 19: return value.to_s + when 20: return value.to_s + when 21: return value.to_s + when 22: return value.to_s + else + raise ValueError, "Invalid type code: %d" % typecode + end + end + + private + + def binding_keys + key_list = [] + key_list << "schema.#" + if @rcv_objects && @rcv_events && @rcv_heartbeats && + ! @user_bindings + key_list << "console.#" + else + if @rcv_objects && ! @user_bindings + key_list << "console.obj.#" + else + key_list << "console.obj.*.*.org.apache.qpid.broker.agent" + end + key_list << "console.event.#" if @rcv_events + key_list << "console.heartbeat.#" if @rcv_heartbeats + end + return key_list + end + + # Check the object against select to check for a match + def select_match(object) + select.each do |key, value| + object.properties.each do |prop, propval| + return false if key == prop.name && value != propval + end + end + return true + end + + end + + class Package + attr_reader :name + + def initialize(name) + @name = name + end + end + + # A ClassKey uniquely identifies a class from the schema. + class ClassKey + attr_reader :package, :klass_name, :hash + + def initialize(package="", klass_name="", hash=0) + if (package.kind_of?(Qpid::Codec)) + @package = package.read_str8() + @klass_name = package.read_str8() + @hash = package.read_bin128() + else + @package = package + @klass_name = klass_name + @hash = hash + end + end + + def encode(codec) + codec.write_str8(@package) + codec.write_str8(@klass_name) + codec.write_bin128(@hash) + end + + def to_a() + return [@package, @klass_name, @hash] + end + + def hash_string() + "%08x-%08x-%08x-%08x" % hash.unpack("NNNN") + end + + def to_s() + return "#{@package}:#{@klass_name}(#{hash_string()})" + end + end + + class SchemaClass + + CLASS_KIND_TABLE = 1 + CLASS_KIND_EVENT = 2 + + attr_reader :klass_key, :arguments, :super_klass_key + + def initialize(session, kind, key, codec) + @session = session + @kind = kind + @klass_key = key + @super_klass_key = nil + @properties = [] + @statistics = [] + @methods = [] + @arguments = [] + + has_supertype = 0 #codec.read_uint8 + if @kind == CLASS_KIND_TABLE + prop_count = codec.read_uint16 + stat_count = codec.read_uint16 + method_count = codec.read_uint16 + if has_supertype == 1 + @super_klass_key = ClassKey.new(codec) + end + prop_count.times { |idx| + @properties << SchemaProperty.new(codec) } + stat_count.times { |idx| + @statistics << SchemaStatistic.new(codec) } + method_count.times { |idx| + @methods<< SchemaMethod.new(codec) } + elsif @kind == CLASS_KIND_EVENT + arg_count = codec.read_uint16 + arg_count.times { |idx| + sa = SchemaArgument.new(codec, false) + @arguments << sa + } + end + end + + def is_table? + @kind == CLASS_KIND_TABLE + end + + def is_event? + @kind == CLASS_KIND_EVENT + end + + def properties(include_inherited = true) + returnValue = @properties + if !@super_klass_key.nil? && include_inherited + returnValue = @properties + @session.schema(@super_klass_key).properties + end + return returnValue + end + + def statistics(include_inherited = true) + returnValue = @statistics + if !@super_klass_key.nil? && include_inherited + returnValue = @statistics + @session.schema(@super_klass_key).statistics + end + return returnValue + end + + def methods(include_inherited = true) + returnValue = @methods + if !@super_klass_key.nil? && include_inherited + returnValue = @methods + @session.schema(@super_klass_key).methods + end + return returnValue + end + + def to_s + if @kind == CLASS_KIND_TABLE + kind_str = "Table" + elsif @kind == CLASS_KIND_EVENT + kind_str = "Event" + else + kind_str = "Unsupported" + end + "#{kind_str} Class: #{klass_key.to_s}" + end + end + + class SchemaProperty + + attr_reader :name, :type, :access, :index, :optional, + :unit, :min, :max, :maxlen, :desc, :refClass, :refPackage + + def initialize(codec) + map = codec.read_map + @name = map["name"] + @type = map["type"] + @access = map["access"] + @index = map["index"] != 0 + @optional = map["optional"] != 0 + @unit = map["unit"] + @min = map["min"] + @max = map["max"] + @maxlen = map["maxlen"] + @desc = map["desc"] + @refClass = map["refClass"] + @refPackage = map["refPackage"] + end + + def to_s + @name + end + end + + class SchemaStatistic + + attr_reader :name, :type, :unit, :desc, :refClass, :refPackage + + def initialize(codec) + map = codec.read_map + @name = map["name"] + @type = map["type"] + @unit = map["unit"] + @desc = map["desc"] + @refClass = map["refClass"] + @refPackage = map["refPackage"] + end + + def to_s + @name + end + end + + class SchemaMethod + + attr_reader :name, :desc, :arguments + + def initialize(codec) + map = codec.read_map + @name = map["name"] + arg_count = map["argCount"] + @desc = map["desc"] + @arguments = [] + arg_count.times { |idx| + @arguments << SchemaArgument.new(codec, true) + } + end + + def to_s + result = @name + "(" + first = true + result += @arguments.select { |arg| arg.dir.index(?I) }.join(", ") + result += ")" + return result + end + end + + class SchemaArgument + + attr_reader :name, :type, :dir, :unit, :min, :max, :maxlen + attr_reader :desc, :default, :refClass, :refPackage + + def initialize(codec, method_arg) + map = codec.read_map + @name = map["name"] + @type = map["type"] + @dir = map["dir"].upcase if method_arg + @unit = map["unit"] + @min = map["min"] + @max = map["max"] + @maxlen = map["maxlen"] + @desc = map["desc"] + @default = map["default"] + @refClass = map["refClass"] + @refPackage = map["refPackage"] + end + end + + # Object that represents QMF object identifiers + class ObjectId + + include Comparable + + attr_reader :first, :second + + def initialize(codec, first=0, second=0) + if codec + @first = codec.read_uint64 + @second = codec.read_uint64 + else + @first = first + @second = second + end + end + + def <=>(other) + return 1 unless other.is_a?(ObjectId) + return -1 if first < other.first + return 1 if first > other.first + return second <=> other.second + end + + def to_s + "%d-%d-%d-%d-%d" % [flags, sequence, broker_bank, agent_bank, object] + end + + def index + [first, second] + end + + def flags + (first & 0xF000000000000000) >> 60 + end + + def sequence + (first & 0x0FFF000000000000) >> 48 + end + + def broker_bank + (first & 0x0000FFFFF0000000) >> 28 + end + + def agent_bank + first & 0x000000000FFFFFFF + end + + def object + second + end + + def durable? + sequence == 0 + end + + def encode(codec) + codec.write_uint64(first) + codec.write_uint64(second) + end + end + + class Object + + DEFAULT_METHOD_WAIT_TIME = 60 + + attr_reader :object_id, :schema, :properties, :statistics, + :current_time, :create_time, :delete_time, :broker + + def initialize(session, broker, schema, codec, prop, stat, managed=true) + @session = session + @broker = broker + @schema = schema + if managed + @current_time = codec.read_uint64 + @create_time = codec.read_uint64 + @delete_time = codec.read_uint64 + @object_id = ObjectId.new(codec) + end + @properties = [] + @statistics = [] + if prop + missing = parse_presence_masks(codec, schema) + schema.properties.each do |property| + v = nil + unless missing.include?(property.name) + v = @session.decode_value(codec, property.type) + end + @properties << [property, v] + end + end + + if stat + schema.statistics.each do |statistic| + s = @session.decode_value(codec, statistic.type) + @statistics << [statistic, s] + end + end + end + + def klass_key + @schema.klass_key + end + + + def methods + @schema.methods + end + + # Return the current, creation, and deletion times for this object + def timestamps + return [@current_time, @create_time, @delete_time] + end + + # Return a string describing this object's primary key + def index + @properties.select { |property, value| + property.index + }.collect { |property,value| + @session.display_value(value, property.type) }.join(":") + end + + # Replace properties and/or statistics with a newly received update + def merge_update(newer) + unless object_id == newer.object_id + raise "Objects with different object-ids" + end + @properties = newer.properties unless newer.properties.empty? + @statistics = newer.statistics unless newer.statistics.empty? + end + + def update + obj = @session.object(:object_id => @object_id, :broker => @broker) + if obj + merge_update(obj) + else + raise "Underlying object no longer exists." + end + end + + def to_s + @schema.klass_key.to_s + end + + # This must be defined because ruby has this (deprecated) method built in. + def id + method_missing(:id) + end + + # Same here.. + def type + method_missing(:type) + end + + def name + method_missing(:name) + end + + def method_missing(name, *args) + name = name.to_s + + if method = @schema.methods.find { |method| name == method.name } + return invoke(method, name, args) + end + + @properties.each do |property, value| + return value if name == property.name + if name == "_#{property.name}_" && property.type == 10 + # Dereference references + deref = @session.objects(:object_id => value, :broker => @broker) + return nil unless deref.size == 1 + return deref[0] + end + end + @statistics.each do |statistic, value| + if name == statistic.name + return value + end + end + raise "Type Object has no attribute '#{name}'" + end + + def encode(codec) + codec.write_uint8(20) + @schema.klass_key.encode(codec) + + # emit presence masks for optional properties + mask = 0 + bit = 0 + schema.properties.each do |property| + if prop.optional + bit = 1 if bit == 0 + mask |= bit if value + bit = bit << 1 + if bit == 256 + bit = 0 + codec.write_uint8(mask) + mask = 0 + end + codec.write_uint8(mask) if bit != 0 + end + end + + # encode properties + @properties.each do |property, value| + @session.encode_value(codec, value, prop.type) if value + end + + # encode statistics + @statistics.each do |statistic, value| + @session.encode_value(codec, value, stat.type) + end + end + + private + + def send_method_request(method, name, args, synchronous = false, time_wait = nil) + @schema.methods.each do |schema_method| + if name == schema_method.name + send_codec = Qpid::StringCodec.new(@broker.conn.spec) + seq = @session.seq_mgr.reserve([schema_method, synchronous]) + @broker.set_header(send_codec, ?M, seq) + @object_id.encode(send_codec) + @schema.klass_key.encode(send_codec) + send_codec.write_str8(name) + + formals = method.arguments.select { |arg| arg.dir.index(?I) } + count = method.arguments.select { |arg| arg.dir.index(?I) }.size + unless formals.size == args.size + raise "Incorrect number of arguments: expected #{formals.size}, got #{args.size}" + end + + formals.zip(args).each do |formal, actual| + @session.encode_value(send_codec, actual, formal.type) + end + + ttl = time_wait ? time_wait * 1000 : nil + smsg = @broker.message(send_codec.encoded, + "agent.#{object_id.broker_bank}.#{object_id.agent_bank}", ttl=ttl) + @broker.sync_start if synchronous + @broker.emit(smsg) + + return seq + end + end + end + + def invoke(method, name, args) + kwargs = args[args.size - 1] + sync = true + timeout = DEFAULT_METHOD_WAIT_TIME + + if kwargs.class == Hash + if kwargs.include?(:timeout) + timeout = kwargs[:timeout] + end + + if kwargs.include?(:async) + sync = !kwargs[:async] + end + args.pop + end + + seq = send_method_request(method, name, args, synchronous = sync) + if seq + return seq unless sync + unless @broker.wait_for_sync_done(timeout) + @session.seq_mgr.release(seq) + raise "Timed out waiting for method to respond" + end + + if @broker.error + error_text = @broker.error + @broker.error = nil + raise error_text + end + + return @broker.sync_result + end + raise "Invalid Method (software defect) [#{name}]" + end + + def parse_presence_masks(codec, schema) + exclude_list = [] + bit = 0 + schema.properties.each do |property| + if property.optional + if bit == 0 + mask = codec.read_uint8 + bit = 1 + end + if (mask & bit) == 0 + exclude_list << property.name + end + bit *= 2 + bit = 0 if bit == 256 + end + end + return exclude_list + end + end + + class MethodResult + + attr_reader :status, :text, :out_args + + def initialize(status, text, out_args) + @status = status + @text = text + @out_args = out_args + end + + def method_missing(name) + name = name.to_s() + if @out_args.include?(name) + return @out_args[name] + else + raise "Unknown method result arg #{name}" + end + end + + def to_s + argsString = "" + padding = "" + out_args.each do |key,value| + argsString += padding + padding = " " if padding == "" + argsString += key.to_s + argsString += " => " + argsString += value.to_s() + end + "MethodResult(Msg: '#{text}' Status: #{status} Return: [#{argsString}])" + end + end + + class ManagedConnection + + DELAY_MIN = 1 + DELAY_MAX = 128 + DELAY_FACTOR = 2 + include MonitorMixin + + def initialize(broker) + super() + @broker = broker + @cv = new_cond + @is_cancelled = false + end + + # Main body of the running thread. + def start + @thread = Thread.new { + delay = DELAY_MIN + while true + begin + @broker.try_to_connect + synchronize do + while !@is_cancelled and @broker.connected? + @cv.wait + Thread.exit if @is_cancelled + delay = DELAY_MIN + end + end + + rescue + delay *= DELAY_FACTOR if delay < DELAY_MAX + end + + synchronize do + @cv.wait(delay) + Thread.exit if @is_cancelled + end + end + } + end + + # Tell this thread to stop running and return. + def stop + synchronize do + @is_cancelled = true + @cv.signal + end + end + + # Notify the thread that the connection was lost. + def disconnected + synchronize do + @cv.signal + end + end + + def join + @thread.join + end + end + + class Broker + + SYNC_TIME = 60 + @@next_seq = 1 + + include MonitorMixin + + attr_accessor :error + + attr_reader :amqp_session_id, :amqp_session, :conn, :broker_bank, :topic_name + + attr_accessor :broker_id, :sync_result + + def initialize(session, host, port, auth_name, auth_pass, kwargs) + super() + + # For debugging.. + Thread.abort_on_exception = true + + @session = session + @host = host + @port = port + @auth_name = auth_name + @auth_pass = auth_pass + @user_id = nil + @auth_mechanism = kwargs[:mechanism] + @auth_service = kwargs[:service] + @broker_bank = 1 + @topic_bound = false + @cv = new_cond + @error = nil + @broker_id = nil + @is_connected = false + @amqp_session_id = "%s.%d.%d" % [Socket.gethostname, Process::pid, @@next_seq] + @@next_seq += 1 + @conn = nil + if @session.managedConnections? + @thread = ManagedConnection.new(self) + @thread.start + else + @thread = nil + try_to_connect + end + end + + def connected? + @is_connected + end + + def agent(broker_bank, agent_bank) + bank_key = "%d.%d" % [broker_bank, agent_bank] + return @agents[bank_key] + end + + # Get the list of agents reachable via this broker + def agents + @agents.values + end + + def url + "#{@host}:#{@port}" + end + + def to_s + if connected? + "Broker connected at: #{url}" + else + "Disconnected Broker" + end + end + + def wait_for_sync_done(timeout=nil) + wait_time = timeout ? timeout : SYNC_TIME + synchronize do + return @cv.wait_for(wait_time) { ! @sync_in_flight || @error } + end + end + + def wait_for_stable + synchronize do + return unless connected? + return if @reqs_outstanding == 0 + @sync_in_flight = true + unless @cv.wait_for(SYNC_TIME) { @reqs_outstanding == 0 } + raise "Timed out waiting for broker to synchronize" + end + end + end + + # Compose the header of a management message + def set_header(codec, opcode, seq=0) + codec.write_uint8(?A) + codec.write_uint8(?M) + codec.write_uint8(?2) + codec.write_uint8(opcode) + codec.write_uint32(seq) + end + + def message(body, routing_key="broker", ttl=nil) + dp = @amqp_session.delivery_properties + dp.routing_key = routing_key + dp.ttl = ttl if ttl + mp = @amqp_session.message_properties + mp.content_type = "x-application/qmf" + mp.reply_to = amqp_session.reply_to("amq.direct", @reply_name) + #mp.user_id = @user_id if @user_id + return Qpid::Message.new(dp, mp, body) + end + + def emit(msg, dest="qpid.management") + @amqp_session.message_transfer(:destination => dest, + :message => msg) + end + + def inc_outstanding + synchronize { @reqs_outstanding += 1 } + end + + def dec_outstanding + synchronize do + @reqs_outstanding -= 1 + if @reqs_outstanding == 0 && ! @topic_bound + @topic_bound = true + @session.binding_key_list.each do |key| + args = { + :exchange => "qpid.management", + :queue => @topic_name, + :binding_key => key } + @amqp_session.exchange_bind(args) + end + end + if @reqs_outstanding == 0 && @sync_in_flight + sync_done + end + end + end + + def sync_start + synchronize { @sync_in_flight = true } + end + + def sync_done + synchronize do + @sync_in_flight = false + @cv.signal + end + end + + def update_agent(obj) + bank_key = "%d.%d" % [obj.brokerBank, obj.agentBank] + if obj.delete_time == 0 + unless @agents.include?(bank_key) + agent = Agent.new(self, obj.agentBank, obj.label) + @agents[bank_key] = agent + @session.console.new_agent(agent) if @session.console + end + else + agent = @agents.delete(bank_key) + @session.console.del_agent(agent) if agent && @session.console + end + end + + def shutdown + if @thread + @thread.stop + @thread.join + end + if connected? + @amqp_session.incoming("rdest").stop + if @session.console + @amqp_session.incoming("tdest").stop + end + @amqp_session.close + @is_connected = false + end + end + + def try_to_connect + @agents = {} + @agents["1.0"] = Agent.new(self, 0, "BrokerAgent") + @topic_bound = false + @sync_in_flight = false + @sync_request = 0 + @sync_result = nil + @reqs_outstanding = 1 + + # FIXME: Need sth for Qpid::Util::connect + + @conn = Qpid::Connection.new(TCPSocket.new(@host, @port), + :mechanism => @auth_mechanism, + :username => @auth_name, + :password => @auth_pass, + :host => @host, + :service => @auth_service) + @conn.start + @user_id = @conn.user_id + @reply_name = "reply-%s" % amqp_session_id + @amqp_session = @conn.session(@amqp_session_id) + @amqp_session.auto_sync = true + + @amqp_session.queue_declare(:queue => @reply_name, + :exclusive => true, + :auto_delete => true) + + @amqp_session.exchange_bind(:exchange => "amq.direct", + :queue => @reply_name, + :binding_key => @reply_name) + @amqp_session.message_subscribe(:queue => @reply_name, + :destination => "rdest", + :accept_mode => @amqp_session.message_accept_mode.none, + :acquire_mode => @amqp_session.message_acquire_mode.pre_acquired) + q = @amqp_session.incoming("rdest") + q.exc_listen(& method(:exception_cb)) + q.listen(& method(:reply_cb)) + @amqp_session.message_set_flow_mode(:destination => "rdest", + :flow_mode => 1) + @amqp_session.message_flow(:destination => "rdest", + :unit => 0, + :value => 0xFFFFFFFF) + @amqp_session.message_flow(:destination => "rdest", + :unit => 1, + :value => 0xFFFFFFFF) + + @topic_name = "topic-#{@amqp_session_id}" + @amqp_session.queue_declare(:queue => @topic_name, + :exclusive => true, + :auto_delete => true) + @amqp_session.message_subscribe(:queue => @topic_name, + :destination => "tdest", + :accept_mode => @amqp_session.message_accept_mode.none, + :acquire_mode => @amqp_session.message_acquire_mode.pre_acquired) + @amqp_session.incoming("tdest").listen(& method(:reply_cb)) + @amqp_session.message_set_flow_mode(:destination => "tdest", + :flow_mode => 1) + @amqp_session.message_flow(:destination => "tdest", + :unit => 0, + :value => 0xFFFFFFFF) + @amqp_session.message_flow(:destination => "tdest", + :unit => 1, + :value => 0xFFFFFFFF) + + @is_connected = true + @session.handle_broker_connect(self) + + codec = Qpid::StringCodec.new(@conn.spec) + set_header(codec, ?B) + msg = message(codec.encoded) + emit(msg) + end + + private + + # Check the header of a management message and extract the opcode and + # class + def check_header(codec) + begin + return [nil, nil] unless codec.read_uint8 == ?A + return [nil, nil] unless codec.read_uint8 == ?M + return [nil, nil] unless codec.read_uint8 == ?2 + opcode = codec.read_uint8 + seq = codec.read_uint32 + return [opcode, seq] + rescue + return [nil, nil] + end + end + + def reply_cb(msg) + codec = Qpid::StringCodec.new(@conn.spec, msg.body) + loop do + opcode, seq = check_header(codec) + return unless opcode + case opcode + when ?b: @session.handle_broker_resp(self, codec, seq) + when ?p: @session.handle_package_ind(self, codec, seq) + when ?z: @session.handle_command_complete(self, codec, seq) + when ?q: @session.handle_class_ind(self, codec, seq) + when ?m: @session.handle_method_resp(self, codec, seq) + when ?h: @session.handle_heartbeat_ind(self, codec, seq, msg) + when ?e: @session.handle_event_ind(self, codec, seq) + when ?s: @session.handle_schema_resp(self, codec, seq) + when ?c: @session.handle_content_ind(self, codec, seq, true, false) + when ?i: @session.handle_content_ind(self, codec, seq, false, true) + when ?g: @session.handle_content_ind(self, codec, seq, true, true) + else + raise "Unexpected opcode #{opcode.inspect}" + end + end + end + + def exception_cb(data) + @is_connected = false + @error = data + synchronize { @cv.signal if @sync_in_flight } + @session.handle_error(@error) + @session.handle_broker_disconnect(self) + @thread.disconnected if @thread + end + end + + class Agent + attr_reader :broker, :agent_bank, :label + + def initialize(broker, agent_bank, label) + @broker = broker + @agent_bank = agent_bank + @label = label + end + + def broker_bank + @broker.broker_bank + end + + def to_s + "Agent at bank %d.%d (%s)" % [@broker.broker_bank, @agent_bank, @label] + end + end + + class Event + + attr_reader :klass_key, :arguments, :timestamp, :name, :schema + + def initialize(session, broker, codec) + @session = session + @broker = broker + @klass_key = ClassKey.new(codec) + @timestamp = codec.read_int64 + @severity = codec.read_uint8 + @schema = nil + + pname, cname, hash = @klass_key.to_a() + session.packages.keys.each do |pname| + k = [cname, hash] + if session.packages[pname].include?(k) + @schema = session.packages[pname][k] + @arguments = {} + @schema.arguments.each do |arg| + v = session.decode_value(codec, arg.type) + @arguments[arg.name] = v + end + end + end + end + + def to_s + return "<uninterpretable>" unless @schema + t = Time.at(self.timestamp / 1000000000) + out = t.strftime("%c") + out += " " + sev_name + " " + @klass_key.package + ":" + @klass_key.klass_name + out += " broker=" + @broker.url + @schema.arguments.each do |arg| + out += " " + arg.name + "=" + @session.display_value(@arguments[arg.name], arg.type) + end + return out + end + + def sev_name + case @severity + when 0 : return "EMER " + when 1 : return "ALERT" + when 2 : return "CRIT " + when 3 : return "ERROR" + when 4 : return "WARN " + when 5 : return "NOTIC" + when 6 : return "INFO " + when 7 : return "DEBUG" + else + return "INV-%d" % @severity + end + end + + end + + # Manage sequence numbers for asynchronous method calls + class SequenceManager + include MonitorMixin + + def initialize + super() + @sequence = 0 + @pending = {} + end + + # Reserve a unique sequence number + def reserve (data) + synchronize do + result = @sequence + @sequence += 1 + @pending[result] = data + return result + end + end + + # Release a reserved sequence number + def release (seq) + synchronize { @pending.delete(seq) } + end + end + + class DebugConsole < Console + + def broker_connected(broker) + puts "brokerConnected #{broker}" + end + + def broker_disconnected(broker) + puts "brokerDisconnected #{broker}" + end + + def new_package(name) + puts "newPackage #{name}" + end + + def new_class(kind, klass_key) + puts "newClass #{kind} #{klass_key}" + end + + def new_agent(agent) + puts "new_agent #{agent}" + end + + def del_agent(agent) + puts "delAgent #{agent}" + end + + def object_props(broker, record) + puts "objectProps #{record}" + end + + def object_stats(broker, record) + puts "objectStats #{record}" + end + + def event(broker, event) + puts "event #{event}" + end + + def heartbeat(agent, timestamp) + puts "heartbeat #{agent}" + end + + def broker_info(broker) + puts "brokerInfo #{broker}" + end + end + + module XML + TYPES = { + 1 => "uint8", + 2 => "uint16", + 3 => "uint32", + 4 => "uint64", + 5 => "bool", + 6 => "short-stirng", + 7 => "long-string", + 8 => "abs-time", + 9 => "delta-time", + 10 => "reference", + 11 => "boolean", + 12 => "float", + 13 => "double", + 14 => "uuid", + 15 => "field-table", + 16 => "int8", + 17 => "int16", + 18 => "int32", + 19 => "int64", + 20 => "object", + 21 => "list", + 22 => "array" + } + + ACCESS_MODES = { + 1 => "RC", + 2 => "RW", + 3 => "RO" + } + + def common_attributes(item) + attr_string = "" + attr_string << " desc='#{item.desc}'" if item.desc + attr_string << " desc='#{item.desc}'" if item.desc + attr_string << " refPackage='#{item.refPackage}'" if item.refPackage + attr_string << " refClass='#{item.refClass}'" if item.refClass + attr_string << " unit='#{item.unit}'" if item.unit + attr_string << " min='#{item.min}'" if item.min + attr_string << " max='#{item.max}'" if item.max + attr_string << " maxlen='#{item.maxlen}'" if item.maxlen + return attr_string + end + + module_function :common_attributes + + def schema_xml(session, *packages) + schema = "<schemas>\n" + packages.each do |package| + schema << "\t<schema package='#{package}'>\n" + session.classes(package).each do |klass_key| + klass = session.schema(klass_key) + if klass.is_table? + if klass.super_klass_key + schema << "\t\t<class name='#{klass.klass_key.klass_name}' hash='#{klass.klass_key.hash_string}' extends='#{klass.super_klass_key.to_s}'>\n" + else + schema << "\t\t<class name='#{klass.klass_key.klass_name}' hash='#{klass.klass_key.hash_string}'>\n" + end + klass.properties(false).each do |property| + schema << "\t\t\t<property name='#{property.name}' type='#{TYPES[property.type]}' access='#{ACCESS_MODES[property.access]}' optional='#{property.optional ? "True" : "False"}'#{common_attributes(property)}/>\n" + end + klass.methods(false).each do |method| + schema << "\t\t\t<method name='#{method.name}'>\n" + method.arguments.each do |arg| + schema << "\t\t\t\t<arg name='#{arg.name}' dir='#{arg.dir}' type='#{TYPES[arg.type]}'#{common_attributes(arg)}/>\n" + end + schema << "\t\t\t</method>\n" + end + schema << "\t\t</class>\n" + else + schema << "\t\t<event name='#{klass.klass_key.klass_name}' hash='#{klass.klass_key.hash_string}'>\n" + klass.arguments.each do |arg| + schema << "\t\t\t<arg name='#{arg.name}'type='#{TYPES[arg.type]}'#{common_attributes(arg)}/>\n" + end + schema << "\t\t</event>\n" + end + end + schema << "\t</package>\n" + end + schema << "</schema>" + end + + module_function :schema_xml + end + +end diff --git a/qpid/ruby/lib/qpid/queue.rb b/qpid/ruby/lib/qpid/queue.rb new file mode 100644 index 0000000000..4150173b53 --- /dev/null +++ b/qpid/ruby/lib/qpid/queue.rb @@ -0,0 +1,101 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +# Augment the standard python multithreaded Queue implementation to add a +# close() method so that threads blocking on the content of a queue can be +# notified if the queue is no longer in use. + +require 'thread' + +# Python nominally uses a bounded queue, but the code never establishes +# a maximum size; we therefore use Ruby's unbounded queue +class Qpid::Queue < ::Queue + + DONE = Object.new + STOP = Object.new + + def initialize + super + @error = nil + @listener = nil + @exc_listener = nil + @exc_listener_lock = Monitor.new + @thread = nil + end + + def close(error = nil) + @error = error + put(DONE) + unless @thread.nil? + @thread.join() + @thread = nil + end + end + + def get(block = true, timeout = nil) + unless timeout.nil? + raise NotImplementedError + end + result = pop(! block) + if result == DONE + # this guarantees that any other waiting threads or any future + # calls to get will also result in a Qpid::Closed exception + put(DONE) + raise Qpid::Closed.new(@error) + else + return result + end + end + + alias :put :push + + def exc_listen(&block) + @exc_listener_lock.synchronize do + @exc_listener = block + end + end + + def listen(&block) + if ! block_given? && @thread + put(STOP) + @thread.join() + @thread = nil + end + + # FIXME: There is a potential race since we could be changing one + # non-nil listener to another + @listener = block + + if block_given? && @thread.nil? + @thread = Thread.new do + loop do + begin + o = get() + break if o == STOP + @listener.call(o) + rescue Qpid::Closed => e + @exc_listener.call(e) if @exc_listener + break + end + end + end + end + end + +end diff --git a/qpid/ruby/lib/qpid/session.rb b/qpid/ruby/lib/qpid/session.rb new file mode 100644 index 0000000000..d693b722c2 --- /dev/null +++ b/qpid/ruby/lib/qpid/session.rb @@ -0,0 +1,458 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 'monitor' + +module Qpid + + class Session < Invoker + + def log; Qpid::logger["qpid.io.cmd"]; end + def msg; Qpid::logger["qpid.io.msg"]; end + + + class Exception < RuntimeError; end + class Closed < Qpid::Session::Exception; end + class Detached < Qpid::Session::Exception; end + + + INCOMPLETE = Object.new + + def self.client(*args) + return Qpid::Client(*args) + end + + def self.server(*args) + return Server(*args) + end + + attr_reader :name, :spec, :auto_sync, :timeout, :channel + attr_reader :results, :exceptions + attr_accessor :channel, :auto_sync, :send_id, :receiver, :sender + + # FIXME: Pass delegate through a block ? + def initialize(name, spec, kwargs = {}) + auto_sync = true + auto_sync = kwargs[:auto_sync] if kwargs.key?(:auto_sync) + timeout = kwargs[:timeout] || 10 + delegate = kwargs[:delegate] + + @name = name + @spec = spec + @auto_sync = auto_sync + @timeout = timeout + @invoke_lock = Monitor.new + @closing = false + @closed = false + + @cond_lock = Monitor.new + @condition = @cond_lock.new_cond + + @send_id = true + @receiver = Receiver.new(self) + @sender = Sender.new(self) + + @lock = Monitor.new + @incoming = {} + @results = {} + @exceptions = [] + + @assembly = nil + + @delegate = delegate.call(self) if delegate + + @ctl_seg = spec[:segment_type].enum[:control].value + @cmd_seg = spec[:segment_type].enum[:command].value + @hdr_seg = spec[:segment_type].enum[:header].value + @body_seg = spec[:segment_type].enum[:body].value + end + + def incoming(destination) + @lock.synchronize do + queue = @incoming[destination] + unless queue + queue = Incoming.new(self, destination) + @incoming[destination] = queue + end + return queue + end + end + + def error? + @exceptions.size > 0 + end + + def sync(timeout=nil) + if channel && Thread.current == channel.connection.thread + raise Qpid::Session::Exception, "deadlock detected" + end + unless @auto_sync + execution_sync(:sync => true) + end + last = @sender.next_id - 1 + @cond_lock.synchronize do + unless @condition.wait_for(timeout) { + @sender.completed.include?(last) || error? + } + raise Qpid::Timeout + end + end + if error? + raise Qpid::Session::Exception, @exceptions + end + end + + def close(timeout=nil) + @invoke_lock.synchronize do + @closing = true + channel.session_detach(name) + end + @cond_lock.synchronize do + unless @condition.wait_for(timeout) { @closed } + raise Qpid::Timeout + end + end + end + + def closed + @lock.synchronize do + return if @closed + + @results.each { |id, f| f.error(exceptions) } + @results.clear + + @incoming.values.each { |q| q.close(exceptions) } + @closed = true + @cond_lock.synchronize { @condition.signal } + end + end + + def resolve_method(name) + o = @spec.children[name] + case o + when Qpid::Spec010::Command + return invocation(:method, o) + when Qpid::Spec010::Struct + return invocation(:method, o) + when Qpid::Spec010::Domain + return invocation(:value, o.enum) unless o.enum.nil? + end + + matches = @spec.children.select { |x| + x.name.to_s.include?(name.to_s) + }.collect { |x| x.name.to_s }.sort + if matches.size == 0 + msg = nil + elsif matches.size == 1 + msg = "Did you mean #{matches[0]} ? " + else + msg = "Did you mean one of #{matches.join(",")} ? " + end + return invocation(:error, msg) + end + + def invoke(type, args) + # XXX + unless type.respond_to?(:track) + return type.create(*args) + end + @invoke_lock.synchronize do + return do_invoke(type, args) + end + end + + def do_invoke(type, args) + raise Qpid::Session::Closed if @closing + raise Qpid::Session::Detached unless channel + + # Clumsy simulation of Python's keyword args + kwargs = {} + if args.size > 0 && args[-1].is_a?(Hash) + if args.size > type.fields.size + kwargs = args.pop + elsif type.fields[args.size - 1].type != @spec[:map] + kwargs = args.pop + end + end + + if type.payload + if args.size == type.fields.size + 1 + message = args.pop + else + message = kwargs.delete(:message) # XXX Really ? + end + else + message = nil + end + + hdr = Qpid::struct(@spec[:header]) + hdr.sync = @auto_sync || kwargs.delete(:sync) + + cmd = type.create(*args.push(kwargs)) + sc = Qpid::StringCodec.new(@spec) + sc.write_command(hdr, cmd) + + seg = Segment.new(true, (message.nil? || + (message.headers.nil? && message.body.nil?)), + type.segment_type, type.track, @channel.id, sc.encoded) + + unless type.result.nil? + result = Future.new(exception=Exception) + @results[@sender.next_id] = result + end + emit(seg) + + log.debug("SENT %s %s %s" % [seg.id, hdr, cmd]) if log + + unless message.nil? + unless message.headers.nil? + sc = Qpid::StringCodec.new(@spec) + message.headers.each { |st| sc.write_struct32(st) } + + seg = Segment.new(false, message.body.nil?, @hdr_seg, + type.track, @channel.id, sc.encoded) + emit(seg) + end + unless message.body.nil? + seg = Segment.new(false, true, @body_seg, type.track, + @channel.id, message.body) + emit(seg) + end + msg.debug("SENT %s" % message) if msg + end + + if !type.result.nil? + return @auto_sync ? result.get(@timeout) : result + elsif @auto_sync + sync(@timeout) + end + end + + def received(seg) + @receiver.received(seg) + if seg.first_segment? + raise Qpid::Session::Exception unless @assembly.nil? + @assembly = [] + end + @assembly << seg + if seg.last_segment? + dispatch(@assembly) + @assembly = nil + end + end + + def dispatch(assembly) + hdr = nil + cmd = nil + header = nil + body = nil + assembly.each do |seg| + d = seg.decode(@spec) + case seg.type + when @cmd_seg + hdr, cmd = d + when @hdr_seg + header = d + when @body_seg + body = d + else + raise Qpid::Session::Exception + end + end + log.debug("RECV %s %s %s" % [cmd.id, hdr, cmd]) if log + + if cmd.st_type.payload + result = @delegate.send(cmd.st_type.name, cmd, header, body) + else + result = @delegate.send(cmd.st_type.name, cmd) + end + + unless cmd.st_type.result.nil? + execution_result(cmd.id, result) + end + + if result != INCOMPLETE + assembly.each do |seg| + @receiver.has_completed(seg) + # XXX: don't forget to obey sync for manual completion as well + if hdr.sync + @channel.session_completed(@receiver.completed) + end + end + end + end + + # Python calls this 'send', but that has a special meaning + # in Ruby, so we call it 'emit' + def emit(seg) + @sender.emit(seg) + end + + def signal + @cond_lock.synchronize { @condition.signal } + end + + def wait_for(timeout = nil, &block) + @cond_lock.synchronize { @condition.wait_for(timeout, &block) } + end + + def to_s + "<Session: #{name}, #{channel}>" + end + + class Receiver + + attr_reader :completed + attr_accessor :next_id, :next_offset + + def initialize(session) + @session = session + @next_id = nil + @next_offset = nil + @completed = Qpid::RangedSet.new() + end + + def received(seg) + if @next_id.nil? || @next_offset.nil? + raise Exception, "todo" + end + seg.id = @next_id + seg.offset = @next_offset + if seg.last_segment? + @next_id += 1 + @next_offset = 0 + else + @next_offset += seg.payload.size + end + end + + def has_completed(seg) + if seg.id.nil? + raise ArgumentError, "cannot complete unidentified segment" + end + if seg.last_segment? + @completed.add(seg.id) + end + end + + def known_completed(commands) + completed = Qpid::RangedSet.new() + @completed.ranges.each do |c| + unless commands.ranges.find { |kc| + kc.contains(c.lower) && kc.contains(c.upper) + } + completed.add_range(c) + end + end + @completed = completed + end + end + + class Sender + + def initialize(session) + @session = session + @next_id = 0.to_serial + @next_offset = 0 + @segments = [] + @completed = RangedSet.new() + end + + attr_reader :next_id, :completed + + def emit(seg) + seg.id = @next_id + seg.offset = @next_offset + if seg.last_segment? + @next_id += 1 + @next_offset = 0 + else + @next_offset += seg.payload.size + end + @segments << seg + if @session.send_id + @session.send_id = false + @session.channel.session_command_point(seg.id, seg.offset) + end + @session.channel.connection.write_segment(seg) + end + + def has_completed(commands) + @segments = @segments.reject { |seg| commands.include?(seg.id) } + commands.ranges.each do |range| + @completed.add(range.lower, range.upper) + end + end + end + + class Incoming < Qpid::Queue + + def initialize(session, destination) + super() + @session = session + @destination = destination + end + + def start + @session.message_credit_unit.choices.each do |unit| + @session.message_flow(@destination, unit.value, 0xFFFFFFFF) + end + end + + def stop + @session.message_cancel(@destination) + listen # Kill the listener + end + end + + class Delegate + + def initialize(session) + @session = session + end + + #XXX: do something with incoming accepts + def message_accept(ma) nil; end + + def execution_result(er) + future = @session.results.delete(er.command_id) + future.set(er.value) + end + + def execution_exception(ex) + @session.exceptions << ex + end + end + + class Client < Delegate + + def log ; Qpid::logger["qpid.io.msg"]; end + + def message_transfer(cmd, headers, body) + m = Qpid::Message.new(body) + m.headers = headers + m.id = cmd.id + messages = @session.incoming(cmd.destination) + messages.put(m) + log.debug("RECV %s" % m) if log + return INCOMPLETE + end + end + end +end diff --git a/qpid/ruby/lib/qpid/spec.rb b/qpid/ruby/lib/qpid/spec.rb new file mode 100644 index 0000000000..b3d70d019d --- /dev/null +++ b/qpid/ruby/lib/qpid/spec.rb @@ -0,0 +1,183 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "set" +require "rexml/document" +require "qpid/fields" +require "qpid/traverse" + +module Qpid + module Spec + + include REXML + + class Container < Array + + def initialize() + @cache = {} + end + + def [](key) + return @cache[key] if @cache.include?(key) + value = do_lookup(key) + @cache[key] = value + return value + end + + def do_lookup(key) + case key + when String + return find {|x| x.name == key.intern()} + when Symbol + return find {|x| x.name == key} + else + return slice(key) + end + end + + def +(other) + copy = clone() + copy.concat(other) + return copy + end + + end + + class Reference + + fields(:name) + + def init(&block) + @resolver = block + end + + def resolve(*args) + @resolver.call(*args) + end + + end + + class Loader + + def initialize() + @stack = [] + end + + def container() + return Container.new() + end + + def load(obj) + case obj + when String + elem = @stack[-1] + result = container() + elem.elements.each(obj) {|e| + @index = result.size + result << load(e) + } + @index = nil + return result + else + elem = obj + @stack << elem + begin + result = send(:"load_#{elem.name}") + ensure + @stack.pop() + end + return result + end + end + + def element + @stack[-1] + end + + def text + element.text + end + + def attr(name, type = :string, default = nil, path = nil) + if path.nil? + elem = element + else + elem = nil + element.elements.each(path) {|elem|} + if elem.nil? + return default + end + end + + value = elem.attributes[name] + value = value.strip() unless value.nil? + if value.nil? + default + else + send(:"parse_#{type}", value) + end + end + + def parse_int(value) + if value.nil? + return nil + else + value.to_i(0) + end + end + + TRUE = ["yes", "true", "1"].to_set + FALSE = ["no", "false", "0", nil].to_set + + def parse_bool(value) + if TRUE.include?(value) + true + elsif FALSE.include?(value) + false + else + raise Exception.new("parse error, expecting boolean: #{value}") + end + end + + def parse_string(value) + value.to_s + end + + def parse_symbol(value) + value.intern() unless value.nil? + end + + REPLACE = {" " => "_", "-" => "_"} + KEYWORDS = {"global" => "global_", "return" => "return_"} + + def parse_name(value) + return if value.nil? + + REPLACE.each do |k, v| + value = value.gsub(k, v) + end + + value = KEYWORDS[value] if KEYWORDS.has_key? value + return value.intern() + end + + end + + end +end diff --git a/qpid/ruby/lib/qpid/spec010.rb b/qpid/ruby/lib/qpid/spec010.rb new file mode 100644 index 0000000000..3e54115087 --- /dev/null +++ b/qpid/ruby/lib/qpid/spec010.rb @@ -0,0 +1,485 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "qpid/spec" +require 'pathname' +require 'fileutils' + +module Qpid::Spec010 + + include Qpid::Spec + + # XXX: workaround for ruby bug/missfeature + Reference = Reference + Loader = Loader + + class Spec + + ENCODINGS = { + String => "str16", + Fixnum => "int64", + Bignum => "int64", + Float => "float", + NilClass => "void", + Array => "list", + Hash => "map" + } + + fields(:major, :minor, :port, :children) + + def init() + @controls = {} + @commands = {} + @structs = {} + @types = {} + children.each {|c| + case c + when Control + @controls[c.code] = c + when Command + @commands[c.code] = c + when Struct + @structs[c.code] = c + when Type + @types[c.code] = c unless c.code.nil? + end + } + end + + attr_reader :controls, :commands, :structs, :types + + def [](key) + return @children[key] + end + + def encoding(klass) + if ENCODINGS.has_key?(klass) + return self[ENCODINGS[klass]] + end + for base in klass.__bases__ + result = encoding(base) + return result unless result.nil? + end + end + + def inspect; "spec"; end + end + + class Constant + + fields(:name, :value) + + attr :parent, true + + end + + class Type + + fields(:name, :code, :fixed, :variable) + + attr :parent, true + + def present?(value) + if @fixed == 0 + return value + else + return !value.nil? + end + end + + def encode(codec, value) + codec.send("write_#{name}", value) + end + + def decode(codec) + return codec.send("read_#{name}") + end + + def inspect; name; end + + end + + class Domain < Type + + fields(:name, :type, :enum) + + attr :parent, true + + def encode(codec, value) + @type.encode(codec, value) + end + + def decode(codec) + return @type.decode(codec) + end + + end + + class Enum + fields(:choices) + + def [](choice) + case choice + when String + choice = choice.to_sym + return choices.find { |c| c.name == choice } + when Symbol + return choices.find { |c| c.name == choice } + else + return choices.find { |c| c.value == choice } + end + end + + def method_missing(name, *args) + raise ArgumentError.new("wrong number of arguments") unless args.empty? + return self[name].value + end + + end + + class Choice + fields(:name, :value) + end + + class Composite + + fields(:name, :code, :size, :pack, :fields) + + attr :parent, true + + # Python calls this 'new', but that has special meaning in Ruby + def create(*args) + return Qpid::struct(self, *args) + end + + def decode(codec) + codec.read_size(@size) + codec.read_uint16() unless @code.nil? + return Qpid::struct(self, self.decode_fields(codec)) + end + + def decode_fields(codec) + flags = 0 + pack.times {|i| flags |= (codec.read_uint8() << 8*i)} + + result = {} + + fields.each_index {|i| + f = @fields[i] + if flags & (0x1 << i) != 0 + result[f.name] = f.type.decode(codec) + else + result[f.name] = nil + end + } + + return result + end + + def encode(codec, value) + sc = Qpid::StringCodec.new(@spec) + sc.write_uint16(@code) unless @code.nil? + encode_fields(sc, value) + codec.write_size(@size, sc.encoded.size) + codec.write(sc.encoded) + end + + def encode_fields(codec, values) + # FIXME: This could be written cleaner using select + # instead of flags + flags = 0 + fields.each_index do |i| + f = fields[i] + flags |= (0x1 << i) if f.type.present?(values[f.name]) + end + + pack.times { |i| codec.write_uint8((flags >> 8*i) & 0xFF) } + + fields.each_index do |i| + f = fields[i] + f.type.encode(codec, values[f.name]) if flags & (0x1 << i) != 0 + end + end + + def inspect; name; end + + end + + class Field + + fields(:name, :type, :exceptions) + + def default() + return nil + end + + end + + class Struct < Composite + + def present?(value) + return !value.nil? + end + + end + + class Action < Composite; end + + class Control < Action + + def segment_type + @parent[:segment_type].enum[:control].value + end + + def track + @parent[:track].enum[:control].value + end + + end + + class Command < Action + + attr_accessor :payload, :result + + def segment_type + @parent["segment_type"].enum["command"].value + end + + def track + @parent["track"].enum["command"].value + end + + end + + class Doc + fields(:type, :title, :text) + end + + class Loader010 < Loader + + def initialize() + super() + end + + def klass + cls = element + until cls.nil? + break if cls.name == "class" + cls = cls.parent + end + return cls + end + + def scope + if element.name == "struct" + return nil + else + return class_name + end + end + + def class_name + cls = klass + if cls.nil? + return nil + else + return parse_name(cls.attributes["name"].strip) + end + end + + def class_code + cls = klass + if cls.nil? + return 0 + else + return parse_int(cls.attributes["code"].strip) + end + end + + def parse_decl(value) + name = parse_name(value) + + s = scope + if s.nil? + return name + else + return :"#{s}_#{name}" + end + end + + def parse_code(value) + c = parse_int(value) + if c.nil? + return nil + else + return c | (class_code << 8) + end + end + + def parse_type(value) + name = parse_name(value.sub(".", "_")) + cls = class_name + return Reference.new {|spec| + candidates = [name] + candidates << :"#{cls}_#{name}" unless cls.nil? + for c in candidates + child = spec[c] + break unless child.nil? + end + if child.nil? + raise Exception.new("unresolved type: #{name}") + else + child + end +} + end + + def load_amqp() + children = nil + + for s in ["constant", "type", "domain", "struct", "control", + "command"] + ch = load(s) + if children.nil? + children = ch + else + children += ch + end + children += load("class/#{s}") + end + children += load("class/command/result/struct") + Spec.new(attr("major", :int), attr("minor", :int), attr("port", :int), + children) + end + + def load_constant() + Constant.new(attr("name", :decl), attr("value", :int)) + end + + def load_type() + Type.new(attr("name", :decl), attr("code", :code), + attr("fixed-width", :int), attr("variable-width", :int)) + end + + def load_domain() + Domain.new(attr("name", :decl), attr("type", :type), load("enum").first) + end + + def load_enum() + Enum.new(load("choice")) + end + + def load_choice() + Choice.new(attr("name", :name), attr("value", :int)) + end + + def load_field() + Field.new(attr("name", :name), attr("type", :type)) + end + + def load_struct() + Struct.new(attr("name", :decl), attr("code", :code), attr("size", :int), + attr("pack", :int), load("field")) + end + + def load_action(cls) + cls.new(attr("name", :decl), attr("code", :code), 0, 2, load("field")) + end + + def load_control() + load_action(Control) + end + + def load_command() + result = attr("type", :type, nil, "result") + result = attr("name", :type, nil, "result/struct") if result.nil? + segs = load("segments") + cmd = load_action(Command) + cmd.result = result + cmd.payload = !segs.empty? + return cmd + end + + def load_result() + true + end + + def load_segments() + true + end + + end + + def self.spec_cache(specfile) + File::join(File::dirname(__FILE__), "spec_cache", + File::basename(specfile, ".xml") + ".rb_marshal") + end + + # XXX: could be shared + def self.load(spec = nil) + return spec if spec.is_a?(Qpid::Spec010::Spec) + if spec.nil? + # FIXME: Need to add a packaging setup in here so we know where + # the installed spec is going to be. + specfile = nil + if ENV['AMQP_SPEC'] + specfile = ENV['AMQP_SPEC'] + else + require "qpid/config" + specfile = Qpid::Config.amqp_spec + end + else + specfile = spec + end + + specfile_cache = spec_cache(specfile) + # FIXME: Check that cache is newer than specfile + if File::exist?(specfile_cache) + begin + spec = File::open(specfile_cache, "r") do |f| + Marshal::load(f) + end + return spec + rescue + # Ignore, will load from XML + end + end + + doc = File::open(specfile, "r") { |f| Document.new(f) } + spec = Loader010.new().load(doc.root) + spec.traverse! do |o| + if o.is_a?(Reference) + o.resolve(spec) + else + o + end + end + + spec.children.each { |c| c.parent = spec } + + begin + FileUtils::mkdir_p(File::dirname(specfile_cache)) + File::open(specfile_cache, "w") { |f| Marshal::dump(spec, f) } + rescue + # Ignore, we are fine without the cached spec + end + return spec + end + +end diff --git a/qpid/ruby/lib/qpid/spec08.rb b/qpid/ruby/lib/qpid/spec08.rb new file mode 100644 index 0000000000..902c05c297 --- /dev/null +++ b/qpid/ruby/lib/qpid/spec08.rb @@ -0,0 +1,190 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "qpid/spec" + +module Qpid08 + + module Spec + + include Qpid::Spec + + # XXX: workaround for ruby bug/missfeature + Reference = Reference + + class Root + fields(:major, :minor, :classes, :constants, :domains) + + def find_method(name) + classes.each do |c| + c.methods.each do |m| + if name == m.qname + return m + end + end + end + + return nil + end + end + + class Constant + fields(:name, :id, :type, :docs) + end + + class Domain + fields(:name, :type) + end + + class Class + fields(:name, :id, :handler, :fields, :methods, :docs) + end + + class Method + fields(:name, :id, :content?, :responses, :synchronous?, :fields, + :docs) + + def init() + @response = false + end + + attr :parent, true + + def response?; @response end + def response=(b); @response = b end + + def qname + :"#{parent.name}_#{name}" + end + end + + class Field + fields(:name, :id, :type, :docs) + + def default + case type + when :bit then false + when :octet, :short, :long, :longlong then 0 + when :shortstr, :longstr then "" + when :table then {} + end + end + end + + class Doc + fields(:type, :text) + end + + class Container08 < Container + def do_lookup(key) + case key + when Integer + return find {|x| x.id == key} + else + return super(key) + end + end + end + + class Loader08 < Loader + + def container() + return Container08.new() + end + + def load_amqp() + Root.new(attr("major", :int), attr("minor", :int), load("class"), + load("constant"), load("domain")) + end + + def load_class() + Class.new(attr("name", :name), attr("index", :int), attr("handler", :name), + load("field"), load("method"), load("doc")) + end + + def load_method() + Method.new(attr("name", :name), attr("index", :int), + attr("content", :bool), load("response"), + attr("synchronous", :bool), load("field"), load("docs")) + end + + def load_response() + name = attr("name", :name) + Reference.new {|spec, klass| + response = klass.methods[name] + if response.nil? + raise Exception.new("no such method: #{name}") + end + response + } + end + + def load_field() + type = attr("type", :name) + if type.nil? + domain = attr("domain", :name) + type = Reference.new {|spec, klass| + spec.domains[domain].type + } + end + Field.new(attr("name", :name), @index, type, load("docs")) + end + + def load_constant() + Constant.new(attr("name", :name), attr("value", :int), attr("class", :name), + load("doc")) + end + + def load_domain() + Domain.new(attr("name", :name), attr("type", :name)) + end + + def load_doc() + Doc.new(attr("type", :symbol), text) + end + + end + + def self.load(spec) + case spec + when String + spec = File.new(spec) + end + doc = Document.new(spec) + spec = Loader08.new().load(doc.root) + spec.classes.each do |klass| + klass.traverse! do |o| + case o + when Reference + o.resolve(spec, klass) + else + o + end + end + klass.methods.each do |m| + m.parent = klass + m.responses.each do |r| + r.response = true + end + end + end + return spec + end + end +end diff --git a/qpid/ruby/lib/qpid/specs/amqp.0-10-qpid-errata.xml b/qpid/ruby/lib/qpid/specs/amqp.0-10-qpid-errata.xml new file mode 100644 index 0000000000..365928ea4e --- /dev/null +++ b/qpid/ruby/lib/qpid/specs/amqp.0-10-qpid-errata.xml @@ -0,0 +1,6654 @@ +<?xml version="1.0"?> + +<!-- + Copyright Notice + ================ + (c) Copyright Cisco Systems, Credit Suisse, Deutsche Borse Systems, Envoy Technologies, Inc., + Goldman Sachs, IONA Technologies PLC, iMatix Corporation sprl.,JPMorgan Chase Bank Inc. N.A, + Novell, Rabbit Technologies Ltd., Red Hat, Inc., TWIST Process Innovations ltd, and 29West Inc. + 2006, 2007. All rights reserved. + + License + ======= + + Cisco Systems, Credit Suisse, Deutsche Borse Systems, Envoy Technologies, Inc.,Goldman Sachs, + IONA Technologies PLC, iMatix Corporation sprl.,JPMorgan Chase Bank Inc. N.A, Novell, Rabbit + Technologies Ltd., Red Hat, Inc., TWIST Process Innovations ltd, and 29West Inc. (collectively, + the "Authors") each hereby grants to you a worldwide, perpetual, royalty-free, nontransferable, + nonexclusive license to (i) copy, display, distribute and implement the Advanced Messaging Queue + Protocol ("AMQP") Specification and (ii) the Licensed Claims that are held by the Authors, all for + the purpose of implementing the Advanced Messaging Queue Protocol Specification. Your license and + any rights under this Agreement will terminate immediately without notice from any Author if you + bring any claim, suit, demand, or action related to the Advanced Messaging Queue Protocol + Specification against any Author. Upon termination, you shall destroy all copies of the Advanced + Messaging Queue Protocol Specification in your possession or control. + + As used hereunder, "Licensed Claims" means those claims of a patent or patent application, + throughout the world, excluding design patents and design registrations, owned or controlled, or + that can be sublicensed without fee and in compliance with the requirements of this Agreement, by + an Author or its affiliates now or at any future time and which would necessarily be infringed by + implementation of the Advanced Messaging Queue Protocol Specification. A claim is necessarily + infringed hereunder only when it is not possible to avoid infringing it because there is no + plausible non-infringing alternative for implementing the required portions of the Advanced + Messaging Queue Protocol Specification. Notwithstanding the foregoing, Licensed Claims shall not + include any claims other than as set forth above even if contained in the same patent as Licensed + Claims; or that read solely on any implementations of any portion of the Advanced Messaging Queue + Protocol Specification that are not required by the Advanced Messaging Queue Protocol + Specification, or that, if licensed, would require a payment of royalties by the licensor to + unaffiliated third parties. Moreover, Licensed Claims shall not include (i) any enabling + technologies that may be necessary to make or use any Licensed Product but are not themselves + expressly set forth in the Advanced Messaging Queue Protocol Specification (e.g., semiconductor + manufacturing technology, compiler technology, object oriented technology, networking technology, + operating system technology, and the like); or (ii) the implementation of other published + standards developed elsewhere and merely referred to in the body of the Advanced Messaging Queue + Protocol Specification, or (iii) any Licensed Product and any combinations thereof the purpose or + function of which is not required for compliance with the Advanced Messaging Queue Protocol + Specification. For purposes of this definition, the Advanced Messaging Queue Protocol + Specification shall be deemed to include both architectural and interconnection requirements + essential for interoperability and may also include supporting source code artifacts where such + architectural, interconnection requirements and source code artifacts are expressly identified as + being required or documentation to achieve compliance with the Advanced Messaging Queue Protocol + Specification. + + As used hereunder, "Licensed Products" means only those specific portions of products (hardware, + software or combinations thereof) that implement and are compliant with all relevant portions of + the Advanced Messaging Queue Protocol Specification. + + The following disclaimers, which you hereby also acknowledge as to any use you may make of the + Advanced Messaging Queue Protocol Specification: + + THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION IS PROVIDED "AS IS," AND THE AUTHORS MAKE NO + REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS + OF THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION ARE SUITABLE FOR ANY PURPOSE; NOR THAT THE + IMPLEMENTATION OF THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION WILL NOT INFRINGE ANY THIRD + PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + THE AUTHORS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF OR RELATING TO ANY USE, IMPLEMENTATION OR DISTRIBUTION OF THE ADVANCED + MESSAGING QUEUE PROTOCOL SPECIFICATION. + + The name and trademarks of the Authors may NOT be used in any manner, including advertising or + publicity pertaining to the Advanced Messaging Queue Protocol Specification or its contents + without specific, written prior permission. Title to copyright in the Advanced Messaging Queue + Protocol Specification will at all times remain with the Authors. + + No other rights are granted by implication, estoppel or otherwise. + + Upon termination of your license or rights under this Agreement, you shall destroy all copies of + the Advanced Messaging Queue Protocol Specification in your possession or control. + + Trademarks + ========== + "JPMorgan", "JPMorgan Chase", "Chase", the JPMorgan Chase logo and the Octagon Symbol are + trademarks of JPMorgan Chase & Co. + + IMATIX and the iMatix logo are trademarks of iMatix Corporation sprl. + + IONA, IONA Technologies, and the IONA logos are trademarks of IONA Technologies PLC and/or its + subsidiaries. + + LINUX is a trademark of Linus Torvalds. RED HAT and JBOSS are registered trademarks of Red Hat, + Inc. in the US and other countries. + + Java, all Java-based trademarks and OpenOffice.org are trademarks of Sun Microsystems, Inc. in the + United States, other countries, or both. + + Other company, product, or service names may be trademarks or service marks of others. + + Links to full AMQP specification: + ================================= + http://www.envoytech.org/spec/amq/ + http://www.iona.com/opensource/amqp/ + http://www.redhat.com/solutions/specifications/amqp/ + http://www.twiststandards.org/tiki-index.php?page=AMQ + http://www.imatix.com/amqp +--> + +<!-- + XML Notes + ========= + + We use entities to indicate repetition; attributes to indicate properties. + + We use the "name" attribute as an identifier, usually within the context of the surrounding + entities. + + We use hyphens (minus char '-') to seperate words in names. + + We do not enforce any particular validation mechanism but we support all mechanisms. The protocol + definition conforms to a formal grammar that is published seperately in several technologies. + +--> + +<!DOCTYPE amqp SYSTEM "amqp.0-10.dtd"> + +<amqp xmlns="http://www.amqp.org/schema/amqp.xsd" + major="0" minor="10" port="5672"> + + <!-- + ====================== == type definitions == ====================== + --> + + <!-- + 0x00 - 0x0f: Fixed width, 1 octet + --> + + <type name="bin8" code="0x00" fixed-width="1" label="octet of unspecified encoding"> + <doc> + The bin8 type consists of exactly one octet of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET + +----------+ + | bin8 | + +----------+ + </doc> + + <doc type="bnf"> + bin8 = OCTET + </doc> + </type> + + <type name="int8" code="0x01" fixed-width="1" label="8-bit signed integral value (-128 - 127)"> + <doc> + The int8 type is a signed integral value encoded using an 8-bit two's complement + representation. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET + +----------+ + | int8 | + +----------+ + </doc> + + <doc type="bnf"> + int8 = OCTET + </doc> + </type> + + <type name="uint8" code="0x02" fixed-width="1" label="8-bit unsigned integral value (0 - 255)"> + <doc> + The uint8 type is an 8-bit unsigned integral value. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET + +---------+ + | uint8 | + +---------+ + </doc> + + <doc type="bnf"> + uint8 = OCTET + </doc> + </type> + + <type name="char" code="0x04" fixed-width="1" label="an iso-8859-15 character"> + <doc> + The char type encodes a single character from the iso-8859-15 character set. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET + +----------+ + | char | + +----------+ + </doc> + + <doc type="bnf"> + char = OCTET + </doc> + </type> + + <type name="boolean" code="0x08" fixed-width="1" + label="boolean value (zero represents false, nonzero represents true)"> + <doc> + The boolean type is a single octet that encodes a true or false value. If the octet is zero, + then the boolean is false. Any other value represents true. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET + +---------+ + | boolean | + +---------+ + </doc> + + <doc type="bnf"> + boolean = OCTET + </doc> + </type> + + <!-- + 0x10 - 0x1f: Fixed width, 2 octets + --> + + <type name="bin16" code="0x10" fixed-width="2" label="two octets of unspecified binary encoding"> + <doc> + The bin16 type consists of two consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET + +-----------+-----------+ + | octet-one | octet-two | + +-----------+-----------+ + </doc> + + <doc type="bnf"> + bin16 = 2 OCTET + </doc> + </type> + + <type name="int16" code="0x11" fixed-width="2" label="16-bit signed integral value"> + <doc> + The int16 type is a signed integral value encoded using a 16-bit two's complement + representation in network byte order. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET + +-----------+----------+ + | high-byte | low-byte | + +-----------+----------+ + </doc> + + <doc type="bnf"> + int16 = high-byte low-byte + high-byte = OCTET + low-byte = OCTET + </doc> + </type> + + <type name="uint16" code="0x12" fixed-width="2" label="16-bit unsigned integer"> + <doc> + The uint16 type is a 16-bit unsigned integral value encoded in network byte order. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET + +-----------+----------+ + | high-byte | low-byte | + +-----------+----------+ + </doc> + + <doc type="bnf"> + uint16 = high-byte low-byte + high-byte = OCTET + low-byte = OCTET + </doc> + </type> + + <!-- + 0x20 - 0x2f: Fixed width, 4 octets + --> + + <type name="bin32" code="0x20" fixed-width="4" label="four octets of unspecified binary encoding"> + <doc> + The bin32 type consists of 4 consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-------------+------------+ + | octet-one | octet-two | octet-three | octet-four | + +-----------+-----------+-------------+------------+ + </doc> + + <doc type="bnf"> + bin32 = 4 OCTET + </doc> + </type> + + <type name="int32" code="0x21" fixed-width="4" label="32-bit signed integral value"> + <doc> + The int32 type is a signed integral value encoded using a 32-bit two's complement + representation in network byte order. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+------------+----------+----------+ + | byte-four | byte-three | byte-two | byte-one | + +-----------+------------+----------+----------+ + MSB LSB + </doc> + + <doc type="bnf"> + int32 = byte-four byte-three byte-two byte-one + byte-four = OCTET ; most significant byte (MSB) + byte-three = OCTET + byte-two = OCTET + byte-one = OCTET ; least significant byte (LSB) + </doc> + </type> + + <type name="uint32" code="0x22" fixed-width="4" label="32-bit unsigned integral value"> + <doc> + The uint32 type is a 32-bit unsigned integral value encoded in network byte order. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+------------+----------+----------+ + | byte-four | byte-three | byte-two | byte-one | + +-----------+------------+----------+----------+ + MSB LSB + </doc> + + <doc type="bnf"> + uint32 = byte-four byte-three byte-two byte-one + byte-four = OCTET ; most significant byte (MSB) + byte-three = OCTET + byte-two = OCTET + byte-one = OCTET ; least significant byte (LSB) + </doc> + </type> + + <type name="float" code="0x23" fixed-width="4" + label="single precision IEEE 754 32-bit floating point"> + <doc> + The float type encodes a single precision 32-bit floating point number. The format and + operations are defined by the IEEE 754 standard for 32-bit floating point numbers. + </doc> + + <doc type="picture" title="Wire Format"> + 4 OCTETs + +-----------------------+ + | float | + +-----------------------+ + IEEE 754 32-bit float + </doc> + + <doc type="bnf"> + float = 4 OCTET ; IEEE 754 32-bit floating point number + </doc> + </type> + + <type name="char-utf32" code="0x27" fixed-width="4" + label="single unicode character in UTF-32 encoding"> + <doc> + The char-utf32 type consists of a single unicode character in the UTF-32 encoding. + </doc> + + <doc type="picture" title="Wire Format"> + 4 OCTETs + +------------------+ + | char-utf32 | + +------------------+ + UTF-32 character + </doc> + + <doc type="bnf"> + char-utf32 = 4 OCTET ; single UTF-32 character + </doc> + </type> + + <type name="sequence-no" fixed-width="4" label="serial number defined in RFC-1982"> + <doc> + The sequence-no type encodes, in network byte order, a serial number as defined in RFC-1982. + The arithmetic, operators, and ranges for numbers of this type are defined by RFC-1982. + </doc> + + <doc type="picture" title="Wire Format"> + 4 OCTETs + +------------------------+ + | sequence-no | + +------------------------+ + RFC-1982 serial number + </doc> + + <doc type="bnf"> + sequence-no = 4 OCTET ; RFC-1982 serial number + </doc> + </type> + + <!-- + 0x30 - 0x3f: Fixed width types - 8 octets + --> + + <type name="bin64" code="0x30" fixed-width="8" + label="eight octets of unspecified binary encoding"> + <doc> + The bin64 type consists of eight consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-----+-------------+-------------+ + | octet-one | octet-two | ... | octet-seven | octet-eight | + +-----------+-----------+-----+-------------+-------------+ + </doc> + + <doc type="bnf"> + bin64 = 8 OCTET + </doc> + </type> + + <type name="int64" code="0x31" fixed-width="8" label="64-bit signed integral value"> + <doc> + The int64 type is a signed integral value encoded using a 64-bit two's complement + representation in network byte order. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +------------+------------+-----+----------+----------+ + | byte-eight | byte-seven | ... | byte-two | byte-one | + +------------+------------+-----+----------+----------+ + MSB LSB + </doc> + + <doc type="bnf"> + int64 = byte-eight byte-seven byte-six byte-five + byte-four byte-three byte-two byte-one + byte-eight = 1 OCTET ; most significant byte (MSB) + byte-seven = 1 OCTET + byte-six = 1 OCTET + byte-five = 1 OCTET + byte-four = 1 OCTET + byte-three = 1 OCTET + byte-two = 1 OCTET + byte-one = 1 OCTET ; least significant byte (LSB) + </doc> + </type> + + <type name="uint64" code="0x32" fixed-width="8" label="64-bit unsigned integral value"> + <doc> + The uint64 type is a 64-bit unsigned integral value encoded in network byte order. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +------------+------------+-----+----------+----------+ + | byte-eight | byte-seven | ... | byte-two | byte-one | + +------------+------------+-----+----------+----------+ + MSB LSB + </doc> + + <doc type="bnf"> + uint64 = byte-eight byte-seven byte-six byte-five + byte-four byte-three byte-two byte-one + byte-eight = 1 OCTET ; most significant byte (MSB) + byte-seven = 1 OCTET + byte-six = 1 OCTET + byte-five = 1 OCTET + byte-four = 1 OCTET + byte-three = 1 OCTET + byte-two = 1 OCTET + byte-one = 1 OCTET ; least significant byte (LSB) + </doc> + </type> + + <type name="double" code="0x33" fixed-width="8" label="double precision IEEE 754 floating point"> + <doc> + The double type encodes a double precision 64-bit floating point number. The format and + operations are defined by the IEEE 754 standard for 64-bit double precision floating point + numbers. + </doc> + + <doc type="picture" title="Wire Format"> + 8 OCTETs + +-----------------------+ + | double | + +-----------------------+ + IEEE 754 64-bit float + </doc> + + <doc type="bnf"> + double = 8 OCTET ; double precision IEEE 754 floating point number + </doc> + </type> + + <type name="datetime" code="0x38" fixed-width="8" label="datetime in 64 bit POSIX time_t format"> + <doc> + The datetime type encodes a date and time using the 64 bit POSIX time_t format. + </doc> + + <doc type="picture" title="Wire Format"> + 8 OCTETs + +---------------------+ + | datetime | + +---------------------+ + posix time_t format + </doc> + + <doc type="bnf"> + datetime = 8 OCTET ; 64 bit posix time_t format + </doc> + </type> + + <!-- + 0x40 - 0x4f: Fixed width types - 16 octets + --> + + <type name="bin128" code="0x40" fixed-width="16" + label="sixteen octets of unspecified binary encoding"> + <doc> + The bin128 type consists of 16 consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-----+---------------+---------------+ + | octet-one | octet-two | ... | octet-fifteen | octet-sixteen | + +-----------+-----------+-----+---------------+---------------+ + </doc> + + <doc type="bnf"> + bin128 = 16 OCTET + </doc> + </type> + + <type name="uuid" code="0x48" fixed-width="16" label="UUID (RFC-4122 section 4.1.2) - 16 octets"> + <doc> + The uuid type encodes a universally unique id as defined by RFC-4122. The format and + operations for this type can be found in section 4.1.2 of RFC-4122. + </doc> + + <doc type="picture" title="Wire Format"> + 16 OCTETs + +---------------+ + | uuid | + +---------------+ + RFC-4122 UUID + </doc> + + <doc type="bnf"> + uuid = 16 OCTET ; RFC-4122 section 4.1.2 + </doc> + </type> + + <!-- + 0x50 - 0x5f: Fixed width types - 32 octets + --> + + <type name="bin256" code="0x50" fixed-width="32" + label="thirty two octets of unspecified binary encoding"> + <doc> + The bin256 type consists of thirty two consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-----+------------------+------------------+ + | octet-one | octet-two | ... | octet-thirty-one | octet-thirty-two | + +-----------+-----------+-----+------------------+------------------+ + </doc> + + <doc type="bnf"> + bin256 = 32 OCTET + </doc> + </type> + + <!-- + 0x60 - 0x6f: Fixed width types - 64 octets + --> + + <type name="bin512" code="0x60" fixed-width="64" + label="sixty four octets of unspecified binary encoding"> + <doc> + The bin512 type consists of sixty four consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-----+-------------------+------------------+ + | octet-one | octet-two | ... | octet-sixty-three | octet-sixty-four | + +-----------+-----------+-----+-------------------+------------------+ + </doc> + + <doc type="bnf"> + bin512 = 64 OCTET + </doc> + </type> + + <!-- + 0x70 - 0x7f: Fixed width types - 128 octets + --> + + <type name="bin1024" code="0x70" fixed-width="128" + label="one hundred and twenty eight octets of unspecified binary encoding"> + <doc> + The bin1024 type consists of one hundred and twenty eight octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-----+------------------------+------------------------+ + | octet-one | octet-two | ... | octet-one-twenty-seven | octet-one-twenty-eight | + +-----------+-----------+-----+------------------------+------------------------+ + </doc> + + <doc type="bnf"> + bin1024 = 128 OCTET + </doc> + </type> + + <!-- + 0x80 - 0x8f: Variable length - one byte length field (up to 255 octets) + --> + + <type name="vbin8" code="0x80" variable-width="1" label="up to 255 octets of opaque binary data"> + <doc> + The vbin8 type encodes up to 255 octets of opaque binary data. The number of octets is first + encoded as an 8-bit unsigned integral value. This is followed by the actual data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET size OCTETs + +---------+-------------+ + | size | octets | + +---------+-------------+ + uint8 + </doc> + + <doc type="bnf"> + vbin8 = size octets + size = uint8 + octets = 0*255 OCTET ; size OCTETs + </doc> + </type> + + <type name="str8-latin" code="0x84" variable-width="1" label="up to 255 iso-8859-15 characters"> + <doc> + The str8-latin type encodes up to 255 octets of iso-8859-15 characters. The number of octets + is first encoded as an 8-bit unsigned integral value. This is followed by the actual + characters. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET size OCTETs + +---------+------------------------+ + | size | characters | + +---------+------------------------+ + uint16 iso-8859-15 characters + </doc> + + <doc type="bnf"> + str8-latin = size characters + size = uint8 + characters = 0*255 OCTET ; size OCTETs + </doc> + </type> + + <type name="str8" code="0x85" variable-width="1" label="up to 255 octets worth of UTF-8 unicode"> + <doc> + The str8 type encodes up to 255 octets worth of UTF-8 unicode. The number of octets of unicode + is first encoded as an 8-bit unsigned integral value. This is followed by the actual UTF-8 + unicode. Note that the encoded size refers to the number of octets of unicode, not necessarily + the number of characters since the UTF-8 unicode may include multi-byte character sequences. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET size OCTETs + +---------+--------------+ + | size | utf8-unicode | + +---------+--------------+ + uint8 + </doc> + + <doc type="bnf"> + str8 = size utf8-unicode + size = uint8 + utf8-unicode = 0*255 OCTET ; size OCTETs + </doc> + </type> + + <type name="str8-utf16" code="0x86" variable-width="1" + label="up to 255 octets worth of UTF-16 unicode"> + <doc> + The str8-utf16 type encodes up to 255 octets worth of UTF-16 unicode. The number of octets of + unicode is first encoded as an 8-bit unsigned integral value. This is followed by the actual + UTF-16 unicode. Note that the encoded size refers to the number of octets of unicode, not the + number of characters since the UTF-16 unicode will include at least two octets per unicode + character. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET size OCTETs + +---------+---------------+ + | size | utf16-unicode | + +---------+---------------+ + uint8 + </doc> + + <doc type="bnf"> + str8-utf16 = size utf16-unicode + size = uint8 + utf16-unicode = 0*255 OCTET ; size OCTETs + </doc> + </type> + + <!-- + 0x90 - 0x9f: Variable length types - two byte length field (up to 65535 octets) + --> + + <type name="vbin16" code="0x90" variable-width="2" + label="up to 65535 octets of opaque binary data"> + <doc> + The vbin16 type encodes up to 65535 octets of opaque binary data. The number of octets is + first encoded as a 16-bit unsigned integral value in network byte order. This is followed by + the actual data. + </doc> + + <doc type="picture" title="Wire Format"> + 2 OCTETs size OCTETs + +----------+-------------+ + | size | octets | + +----------+-------------+ + uint16 + </doc> + + <doc type="bnf"> + vbin16 = size octets + size = uint16 + octets = 0*65535 OCTET ; size OCTETs + </doc> + </type> + + <type name="str16-latin" code="0x94" variable-width="2" + label="up to 65535 iso-8859-15 characters"> + <doc> + The str16-latin type encodes up to 65535 octets of is-8859-15 characters. The number of octets + is first encoded as a 16-bit unsigned integral value in network byte order. This is followed + by the actual characters. + </doc> + + <doc type="picture" title="Wire Format"> + 2 OCTETs size OCTETs + +----------+------------------------+ + | size | characters | + +----------+------------------------+ + uint16 iso-8859-15 characters + </doc> + + <doc type="bnf"> + str16-latin = size characters + size = uint16 + characters = 0*65535 OCTET ; size OCTETs + </doc> + </type> + + <type name="str16" code="0x95" variable-width="2" + label="up to 65535 octets worth of UTF-8 unicode"> + <doc> + The str16 type encodes up to 65535 octets worth of UTF-8 unicode. The number of octets is + first encoded as a 16-bit unsigned integral value in network byte order. This is followed by + the actual UTF-8 unicode. Note that the encoded size refers to the number of octets of + unicode, not necessarily the number of unicode characters since the UTF-8 unicode may include + multi-byte character sequences. + </doc> + + <doc type="picture" title="Wire Format"> + 2 OCTETs size OCTETs + +----------+--------------+ + | size | utf8-unicode | + +----------+--------------+ + uint16 + </doc> + + <doc type="bnf"> + str16 = size utf8-unicode + size = uint16 + utf8-unicode = 0*65535 OCTET ; size OCTETs + </doc> + </type> + + <type name="str16-utf16" code="0x96" variable-width="2" + label="up to 65535 octets worth of UTF-16 unicode"> + <doc> + The str16-utf16 type encodes up to 65535 octets worth of UTF-16 unicode. The number of octets + is first encoded as a 16-bit unsigned integral value in network byte order. This is followed + by the actual UTF-16 unicode. Note that the encoded size refers to the number of octets of + unicode, not the number of unicode characters since the UTF-16 unicode will include at least + two octets per unicode character. + </doc> + + <doc type="picture" title="Wire Format"> + 2 OCTETs size OCTETs + +----------+---------------+ + | size | utf16-unicode | + +----------+---------------+ + uint16 + </doc> + + <doc type="bnf"> + str16-utf16 = size utf16-unicode + size = uint16 + utf16-unicode = 0*65535 OCTET ; size OCTETs + </doc> + </type> + + <type name="byte-ranges" variable-width="2" label="byte ranges within a 64-bit payload"> + <doc> + The byte-ranges type encodes up to 65535 octets worth of non-overlapping, non-touching, + ascending byte ranges within a 64-bit sequence of bytes. Each range is represented as an + inclusive lower and upper bound that identifies all the byte offsets included within a given + range. + </doc> + + <doc> + The number of octets of data is first encoded as a 16-bit unsigned integral value in network + byte order. This is then followed by the encoded representation of the ranges included in the + set. These MUST be encoded in ascending order, and any two ranges included in a given set MUST + NOT include overlapping or touching byte offsets. + </doc> + + <doc> + Each range is encoded as a pair of 64-bit unsigned integral values in network byte order + respectively representing the lower and upper bounds for that range. Note that because each + range is exactly 16 octets, the size in octets of the encoded ranges will always be 16 times + the number of ranges in the set. + </doc> + + <doc type="picture" title="Wire Format"> + +----= size OCTETs =----+ + | | + 2 OCTETs | 16 OCTETs | + +----------+-----+-----------+-----+ + | size | .../| range |\... | + +----------+---/ +-----------+ \---+ + uint16 / / \ \ + / / \ \ + / 8 OCTETs 8 OCTETs \ + +-----------+-----------+ + | lower | upper | + +-----------+-----------+ + uint64 uint64 + </doc> + + <doc type="bnf"> + byte-ranges = size *range + size = uint16 + range = lower upper + lower = uint64 + upper = uint64 + </doc> + </type> + + <type name="sequence-set" variable-width="2" label="ranged set representation"> + <doc> + The sequence-set type is a set of pairs of RFC-1982 numbers representing a discontinuous range + within an RFC-1982 sequence. Each pair represents a closed interval within the list. + </doc> + + <doc> + Sequence-sets can be represented as lists of pairs of positive 32-bit numbers, each pair + representing a closed interval that does not overlap or touch with any other interval in the + list. For example, a set containing words 0, 1, 2, 5, 6, and 15 can be represented: + </doc> + + <doc type="picture"> + [(0, 2), (5, 6), (15, 15)] + </doc> + + <doc> + 1) The list-of-pairs representation is sorted ascending (as defined by RFC 1982 + (http://www.ietf.org/rfc/rfc1982.txt) ) by the first elements of each pair. + </doc> + + <doc> + 2) The list-of-pairs is flattened into a list-of-words. + </doc> + + <doc> + 3) Each word in the list is packed into ascending locations in memory with network byte + ordering. + </doc> + + <doc> + 4) The size in bytes, represented as a 16-bit network-byte-order unsigned value, is prepended. + </doc> + + <doc> + For instance, the example from above would be encoded: + </doc> + + <doc type="picture"> + [(0, 2), (5, 6), (15, 15)] -- already sorted. + [0, 2, 5, 6, 15, 15] -- flattened. + 000000000000000200000005000000060000000F0000000F -- bytes in hex + 0018000000000000000200000005000000060000000F0000000F -- bytes in hex, + length (24) prepended + </doc> + + <doc type="picture" title="Wire Format"> + +----= size OCTETs =----+ + | | + 2 OCTETs | 8 OCTETs | + +----------+-----+-----------+-----+ + | size | .../| range |\... | + +----------+---/ +-----------+ \---+ + uint16 / / \ \ + / / \ \ + / / \ \ + / / \ \ + / 4 OCTETs 4 OCTETs \ + +-------------+-------------+ + | lower | upper | + +-------------+-------------+ + sequence-no sequence-no + </doc> + + <doc type="bnf"> + sequence-set = size *range + size = uint16 ; length of variable portion in bytes + + range = lower upper ; inclusive + lower = sequence-no + upper = sequence-no + </doc> + </type> + + <!-- + 0xa0 - 0xaf: Variable length types - four byte length field (up to 4294967295 octets) + --> + + <type name="vbin32" code="0xa0" variable-width="4" + label="up to 4294967295 octets of opaque binary data"> + <doc> + The vbin32 type encodes up to 4294967295 octets of opaque binary data. The number of octets is + first encoded as a 32-bit unsigned integral value in network byte order. This is followed by + the actual data. + </doc> + + <doc type="picture" title="Wire Format"> + 4 OCTETs size OCTETs + +----------+-------------+ + | size | octets | + +----------+-------------+ + uint32 + </doc> + + <doc type="bnf"> + vbin32 = size octets + size = uint32 + octets = 0*4294967295 OCTET ; size OCTETs + </doc> + </type> + + <type name="map" code="0xa8" variable-width="4" label="a mapping of keys to typed values"> + <doc> + A map is a set of distinct keys where each key has an associated (type,value) pair. The triple + of the key, type, and value, form an entry within a map. Each entry within a given map MUST + have a distinct key. A map is encoded as a size in octets, a count of the number of entries, + followed by the encoded entries themselves. + </doc> + + <doc> + An encoded map may contain up to (4294967295 - 4) octets worth of encoded entries. The size is + encoded as a 32-bit unsigned integral value in network byte order equal to the number of + octets worth of encoded entries plus 4. (The extra 4 octets is added for the entry count.) The + size is then followed by the number of entries encoded as a 32-bit unsigned integral value in + network byte order. Finally the entries are encoded sequentially. + </doc> + + <doc> + An entry is encoded as the key, followed by the type, and then the value. The key is always a + string encoded as a str8. The type is a single octet that may contain any valid AMQP type + code. The value is encoded according to the rules defined by the type code for that entry. + </doc> + + <doc type="picture" title="Wire Format"> + +------------= size OCTETs =-----------+ + | | + 4 OCTETs | 4 OCTETs | + +----------+----------+-----+---------------+-----+ + | size | count | .../| entry |\... | + +----------+----------+---/ +---------------+ \---+ + uint32 uint32 / / \ \ + / / \ \ + / / \ \ + / / \ \ + / / \ \ + / k OCTETs 1 OCTET n OCTETs \ + +-----------+---------+-----------+ + | key | type | value | + +-----------+---------+-----------+ + str8 *type* + </doc> + + <doc type="bnf"> + map = size count *entry + + size = uint32 ; size of count and entries in octets + count = uint32 ; number of entries in the map + + entry = key type value + key = str8 + type = OCTET ; type code of the value + value = *OCTET ; the encoded value + </doc> + </type> + + <type name="list" code="0xa9" variable-width="4" label="a series of consecutive type-value pairs"> + <doc> + A list is an ordered sequence of (type, value) pairs. The (type, value) pair forms an item + within the list. The list may contain items of many distinct types. A list is encoded as a + size in octets, followed by a count of the number of items, followed by the items themselves + encoded in their defined order. + </doc> + + <doc> + An encoded list may contain up to (4294967295 - 4) octets worth of encoded items. The size is + encoded as a 32-bit unsigned integral value in network byte order equal to the number of + octets worth of encoded items plus 4. (The extra 4 octets is added for the item count.) The + size is then followed by the number of items encoded as a 32-bit unsigned integral value in + network byte order. Finally the items are encoded sequentially in their defined order. + </doc> + + <doc> + An item is encoded as the type followed by the value. The type is a single octet that may + contain any valid AMQP type code. The value is encoded according to the rules defined by the + type code for that item. + </doc> + + <doc type="picture" title="Wire Format"> + +---------= size OCTETs =---------+ + | | + 4 OCTETs | 4 OCTETs | + +----------+----------+-----+----------+-----+ + | size | count | .../| item |\... | + +----------+----------+---/ +----------+ \---+ + uint32 uint32 / / \ \ + / / \ \ + / 1 OCTET n OCTETs \ + +----------+-----------+ + | type | value | + +----------+-----------+ + *type* + </doc> + + <doc type="bnf"> + list = size count *item + + size = uint32 ; size of count and items in octets + count = uint32 ; number of items in the list + + item = type value + type = OCTET ; type code of the value + value = *OCTET ; the encoded value + </doc> + </type> + + <type name="array" code="0xaa" variable-width="4" + label="a defined length collection of values of a single type"> + <doc> + An array is an ordered sequence of values of the same type. The array is encoded in as a size + in octets, followed by a type code, then a count of the number values in the array, and + finally the values encoded in their defined order. + </doc> + + <doc> + An encoded array may contain up to (4294967295 - 5) octets worth of encoded values. The size + is encoded as a 32-bit unsigned integral value in network byte order equal to the number of + octets worth of encoded values plus 5. (The extra 5 octets consist of 4 octets for the count + of the number of values, and one octet to hold the type code for the items in the array.) The + size is then followed by a single octet that may contain any valid AMQP type code. The type + code is then followed by the number of values encoded as a 32-bit unsigned integral value in + network byte order. Finally the values are encoded sequentially in their defined order + according to the rules defined by the type code for the array. + </doc> + + <doc type="picture" title="Wire Format"> + 4 OCTETs 1 OCTET 4 OCTETs (size - 5) OCTETs + +----------+---------+----------+-------------------------+ + | size | type | count | values | + +----------+---------+----------+-------------------------+ + uint32 uint32 *count* encoded *types* + </doc> + + <doc type="bnf"> + array = size type count values + + size = uint32 ; size of type, count, and values in octets + type = OCTET ; the type of the encoded values + count = uint32 ; number of items in the array + + values = 0*4294967290 OCTET ; (size - 5) OCTETs + </doc> + </type> + + <type name="struct32" code="0xab" variable-width="4" label="a coded struct with a 32-bit size"> + <doc> + The struct32 type describes any coded struct with a 32-bit (4 octet) size. The type is + restricted to be only coded structs with a 32-bit size, consequently the first six octets of + any encoded value for this type MUST always contain the size, class-code, and struct-code in + that order. + </doc> + + <doc> + The size is encoded as a 32-bit unsigned integral value in network byte order that is equal to + the size of the encoded field-data, packing-flags, class-code, and struct-code. The class-code + is a single octet that may be set to any valid class code. The struct-code is a single octet + that may be set to any valid struct code within the given class-code. + </doc> + + <doc> + The first six octets are then followed by the packing flags and encoded field data. The + presence and quantity of packing-flags, as well as the specific fields are determined by the + struct definition identified with the encoded class-code and struct-code. + </doc> + + <doc type="picture" title="Wire Format"> + 4 OCTETs 1 OCTET 1 OCTET pack-width OCTETs n OCTETs + +----------+------------+-------------+-------------------+------------+ + | size | class-code | struct-code | packing-flags | field-data | + +----------+------------+-------------+-------------------+------------+ + uint32 + + n = (size - 2 - pack-width) + </doc> + + <doc type="bnf"> + struct32 = size class-code struct-code packing-flags field-data + + size = uint32 + + class-code = OCTET ; zero for top-level structs + struct-code = OCTET ; together with class-code identifies the struct + ; definition which determines the pack-width and + ; fields + + packing-flags = 0*4 OCTET ; pack-width OCTETs + + field-data = *OCTET ; (size - 2 - pack-width) OCTETs + </doc> + </type> + + <!-- + 0xb0 - 0xbf: Reserved + --> + + <!-- + 0xc0 - 0xcf:Fixed width types - 5 octets + --> + + <type name="bin40" code="0xc0" fixed-width="5" label="five octets of unspecified binary encoding"> + <doc> + The bin40 type consists of five consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-------------+------------+------------+ + | octet-one | octet-two | octet-three | octet-four | octet-five | + +-----------+-----------+-------------+------------+------------+ + </doc> + + <doc type="bnf"> + bin40 = 5 OCTET + </doc> + </type> + + <type name="dec32" code="0xc8" fixed-width="5" + label="32-bit decimal value (e.g. for use in financial values)"> + <doc> + The dec32 type is decimal value with a variable number of digits following the decimal point. + It is encoded as an 8-bit unsigned integral value representing the number of decimal places. + This is followed by the signed integral value encoded using a 32-bit two's complement + representation in network byte order. + </doc> + + <doc> + The former value is referred to as the exponent of the divisor. The latter value is the + mantissa. The decimal value is given by: mantissa / 10^exponent. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 4 OCTETs + +----------+----------+ + | exponent | mantissa | + +----------+----------+ + uint8 int32 + </doc> + + <doc type="bnf"> + dec32 = exponent mantissa + exponent = uint8 + mantissa = int32 + </doc> + </type> + + <!-- + 0xd0 - 0xdf: Fixed width types - 9 octets + --> + + <type name="bin72" code="0xd0" fixed-width="9" + label="nine octets of unspecified binary encoding"> + <doc> + The bin72 type consists of nine consecutive octets of opaque binary data. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 1 OCTET 1 OCTET 1 OCTET + +-----------+-----------+-----+-------------+------------+ + | octet-one | octet-two | ... | octet-eight | octet-nine | + +-----------+-----------+-----+-------------+------------+ + </doc> + + <doc type="bnf"> + bin64 = 9 OCTET + </doc> + </type> + + <type name="dec64" code="0xd8" fixed-width="9" + label="64-bit decimal value (e.g. for use in financial values)"> + <doc> + The dec64 type is decimal value with a variable number of digits following the decimal point. + It is encoded as an 8-bit unsigned integral value representing the number of decimal places. + This is followed by the signed integral value encoded using a 64-bit two's complement + representation in network byte order. + </doc> + + <doc> + The former value is referred to as the exponent of the divisor. The latter value is the + mantissa. The decimal value is given by: mantissa / 10^exponent. + </doc> + + <doc type="picture" title="Wire Format"> + 1 OCTET 8 OCTETs + +----------+----------+ + | exponent | mantissa | + +----------+----------+ + uint8 int64 + </doc> + + <doc type="bnf"> + dec64 = exponent mantissa + exponent = uint8 + mantissa = int64 + </doc> + </type> + + <!-- + 0xe0 - 0xef: Reserved + --> + + <!-- + 0xf0 - 0xff: Zero-length types + --> + + <type name="void" code="0xf0" fixed-width="0" label="the void type"> + <doc> + The void type is used within tagged data structures such as maps and lists to indicate an + empty value. The void type has no value and is encoded as an empty sequence of octets. + </doc> + </type> + + <type name="bit" code="0xf1" fixed-width="0" label="presence indicator"> + <doc> + The bit type is used to indicate that a packing flag within a packed struct is being used to + represent a boolean value based on the presence of an empty value. The bit type has no value + and is encoded as an empty sequence of octets. + </doc> + </type> + + <!-- + ====================================================== + == CONSTANTS + ====================================================== + --> + + <!-- Protocol constants --> + + <constant name="MIN-MAX-FRAME-SIZE" value="4096" label="The minimum size (in bytes) which can be + agreed upon as the maximum frame size."> + <doc> + During the initial connection negotiation, the two peers must agree upon a maximum frame size. + This constant defines the minimum value to which the maximum frame size can be set. By + defining this value, the peers can guarantee that they can send frames of up to this size + until they have agreed a definitive maximum frame size for that connection. + </doc> + </constant> + + <!-- + ====================================================== + == DOMAIN TYPES + ====================================================== + --> + + <!-- Segment types --> + + <domain name="segment-type" type="uint8" label="valid values for the frame type indicator."> + <doc> + Segments are defined in <xref ref="specification.transport.assemblies_segments_and_frames"/>. + The segment domain defines the valid values that may be used for the segment indicator within + the frame header. + </doc> + + <enum> + <choice name="control" value="0"> + <doc> + The frame type indicator for Control segments (see <xref + ref="specification.formal_notation.controls"/>). + </doc> + </choice> + <choice name="command" value="1"> + <doc> + The frame type indicator for Command segments (see <xref + ref="specification.formal_notation.commands"/>). + </doc> + </choice> + <choice name="header" value="2" > + <doc> + The frame type indicator for Header segments (see <xref + ref="specification.formal_notation.segments.header"/>). + </doc> + </choice> + <choice name="body" value="3" > + <doc> + The frame type indicator for Body segments (see <xref + ref="specification.formal_notation.segments.body"/>). + </doc> + </choice> + </enum> + </domain> + + <!-- Tracks --> + + <domain name="track" type="uint8" label="Valid values for transport level tracks"> + <doc> Tracks are defined in <xref ref="specification.transport.channels_and_tracks"/>. The + track domain defines the valid values that may used for the track indicator within the frame + header</doc> + <enum> + <choice name="control" value="0"> + <doc> + The track used for all controls. All controls defined in this specification MUST be sent + on track 0. + </doc> + </choice> + <choice name="command" value="1"> + <doc> + The track used for all commands. All commands defined in this specification MUST be sent + on track 1. + </doc> + </choice> + </enum> + </domain> + + + <domain name="str16-array" type="array" label="An array of values of type str16."> + <doc> + An array of values of type str16. + </doc> + </domain> + + + + <!-- == Class: connection ==================================================================== --> + + <class name="connection" code="0x1" label="work with connections"> + <doc> + The connection class provides controls for a client to establish a network connection to a + server, and for both peers to operate the connection thereafter. + </doc> + + <doc type="grammar"> + connection = open-connection + *use-connection + close-connection + open-connection = C:protocol-header + S:START C:START-OK + *challenge + S:TUNE C:TUNE-OK + C:OPEN S:OPEN-OK | S:REDIRECT + challenge = S:SECURE C:SECURE-OK + use-connection = *channel + close-connection = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + </doc> + + <role name="server" implement="MUST" /> + <role name="client" implement="MUST" /> + + <domain name="close-code" type="uint16" label="code used in the connection.close control to + indicate reason for closure"> + <enum> + <choice name="normal" value="200"> + <doc> + The connection closed normally. + </doc> + </choice> + + <choice name="connection-forced" value="320"> + <doc> + An operator intervened to close the connection for some reason. The client may retry at + some later date. + </doc> + </choice> + + <choice name="invalid-path" value="402"> + <doc> + The client tried to work with an unknown virtual host. + </doc> + </choice> + + <choice name="framing-error" value="501"> + <doc> + A valid frame header cannot be formed from the incoming byte stream. + </doc> + </choice> + </enum> + </domain> + + <domain name="amqp-host-url" type="str16" label="URL for identifying an AMQP Server"> + <doc> + The amqp-url domain defines a format for identifying an AMQP Server. It is used to provide + alternate hosts in the case where a client has to reconnect because of failure, or because + the server requests the client to do so upon initial connection. + </doc> + <doc type="bnf"><![CDATA[ + amqp_url = "amqp:" prot_addr_list + prot_addr_list = [prot_addr ","]* prot_addr + prot_addr = tcp_prot_addr | tls_prot_addr + + tcp_prot_addr = tcp_id tcp_addr + tcp_id = "tcp:" | "" + tcp_addr = [host [":" port] ] + host = <as per http://www.ietf.org/rfc/rfc3986.txt> + port = number]]> + </doc> + </domain> + + <domain name="amqp-host-array" type="array" label="An array of values of type amqp-host-url"> + <doc> + Used to provide a list of alternate hosts. + </doc> + </domain> + + <!-- - Control: connection.start - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="start" code="0x1" label="start connection negotiation"> + <doc> + This control starts the connection negotiation process by telling the client the supported + security mechanisms and locales from which the client can choose. + </doc> + + <rule name="protocol-name"> + <doc> + If the server cannot support the protocol specified in the protocol header, it MUST close + the socket connection without sending any response control. + </doc> + <doc type="scenario"> + The client sends a protocol header containing an invalid protocol name. The server must + respond by closing the connection. + </doc> + </rule> + + <rule name="client-support"> + <doc> + If the client cannot handle the protocol version suggested by the server it MUST close the + socket connection. + </doc> + <doc type="scenario"> + The server sends a protocol version that is lower than any valid implementation, e.g. 0.1. + The client must respond by closing the connection. + </doc> + </rule> + + <implement role="client" handle="MUST" /> + + <response name="start-ok" /> + + <field name="server-properties" type="map" label="server properties"> + <rule name="required-fields"> + <doc> + The properties SHOULD contain at least these fields: "host", specifying the server host + name or address, "product", giving the name of the server product, "version", giving the + name of the server version, "platform", giving the name of the operating system, + "copyright", if appropriate, and "information", giving other general information. + </doc> + <doc type="scenario"> + Client connects to server and inspects the server properties. It checks for the presence + of the required fields. + </doc> + </rule> + </field> + + <field name="mechanisms" type="str16-array" label="available security mechanisms" + required="true"> + <doc> + A list of the security mechanisms that the server supports. + </doc> + </field> + + <field name="locales" type="str16-array" label="available message locales" required="true"> + <doc> + A list of the message locales that the server supports. The locale defines the language in + which the server will send reply texts. + </doc> + + <rule name="required-support"> + <doc> + The server MUST support at least the en_US locale. + </doc> + <doc type="scenario"> + Client connects to server and inspects the locales field. It checks for the presence of + the required locale(s). + </doc> + </rule> + </field> + </control> + + <!-- - Control: connection.start-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="start-ok" code="0x2" label="select security mechanism and locale"> + <doc> + This control selects a SASL security mechanism. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="client-properties" type="map" label="client properties"> + <rule name="required-fields"> + <!-- This rule is not testable from the client side --> + <doc> + The properties SHOULD contain at least these fields: "product", giving the name of the + client product, "version", giving the name of the client version, "platform", giving the + name of the operating system, "copyright", if appropriate, and "information", giving + other general information. + </doc> + </rule> + </field> + + <field name="mechanism" type="str8" label="selected security mechanism" required="true"> + <doc> + A single security mechanisms selected by the client, which must be one of those specified + by the server. + </doc> + + <rule name="security"> + <doc> + The client SHOULD authenticate using the highest-level security profile it can handle + from the list provided by the server. + </doc> + </rule> + + <rule name="validity"> + <doc> + If the mechanism field does not contain one of the security mechanisms proposed by the + server in the Start control, the server MUST close the connection without sending any + further data. + </doc> + <doc type="scenario"> + Client connects to server and sends an invalid security mechanism. The server must + respond by closing the connection (a socket close, with no connection close + negotiation). + </doc> + </rule> + </field> + + <field name="response" type="vbin32" label="security response data" required="true"> + <doc> + A block of opaque data passed to the security mechanism. The contents of this data are + defined by the SASL security mechanism. + </doc> + </field> + + <field name="locale" type="str8" label="selected message locale" required="true"> + <doc> + A single message locale selected by the client, which must be one of those specified by + the server. + </doc> + </field> + </control> + + <!-- - Control: connection.secure - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="secure" code="0x3" label="security mechanism challenge"> + <doc> + The SASL protocol works by exchanging challenges and responses until both peers have + received sufficient information to authenticate each other. This control challenges the + client to provide more information. + </doc> + + <implement role="client" handle="MUST" /> + + <response name="secure-ok" /> + + <field name="challenge" type="vbin32" label="security challenge data" required="true"> + <doc> + Challenge information, a block of opaque binary data passed to the security mechanism. + </doc> + </field> + </control> + + <!-- - Control: connection.secure-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="secure-ok" code="0x4" label="security mechanism response"> + <doc> + This control attempts to authenticate, passing a block of SASL data for the security + mechanism at the server side. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="response" type="vbin32" label="security response data" required="true"> + <doc> + A block of opaque data passed to the security mechanism. The contents of this data are + defined by the SASL security mechanism. + </doc> + </field> + </control> + + <!-- - Control: connection.tune - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="tune" code="0x5" label="propose connection tuning parameters"> + <doc> + This control proposes a set of connection configuration values to the client. The client can + accept and/or adjust these. + </doc> + + <implement role="client" handle="MUST" /> + + <response name="tune-ok" /> + + <field name="channel-max" type="uint16" label="proposed maximum channels"> + <doc> + The maximum total number of channels that the server allows per connection. If this is not + set it means that the server does not impose a fixed limit, but the number of allowed + channels may be limited by available server resources. + </doc> + </field> + + <field name="max-frame-size" type="uint16" label="proposed maximum frame size"> + <doc> + The largest frame size that the server proposes for the connection. The client can + negotiate a lower value. If this is not set means that the server does not impose any + specific limit but may reject very large frames if it cannot allocate resources for them. + </doc> + + <rule name="minimum"> + <doc> + Until the max-frame-size has been negotiated, both peers MUST accept frames of up to + MIN-MAX-FRAME-SIZE octets large, and the minimum negotiated value for max-frame-size is + also MIN-MAX-FRAME-SIZE. + </doc> + <doc type="scenario"> + Client connects to server and sends a large properties field, creating a frame of + MIN-MAX-FRAME-SIZE octets. The server must accept this frame. + </doc> + </rule> + </field> + + <field name="heartbeat-min" type="uint16" label="the minimum supported heartbeat delay"> + <doc> + The minimum delay, in seconds, of the connection heartbeat supported by the server. If + this is not set it means the server does not support sending heartbeats. + </doc> + </field> + + <field name="heartbeat-max" type="uint16" label="the maximum supported heartbeat delay"> + <doc> + The maximum delay, in seconds, of the connection heartbeat supported by the server. If + this is not set it means the server has no maximum. + </doc> + + <rule name="permitted-range"> + <doc> + The heartbeat-max value must be greater than or equal to the value supplied in the + heartbeat-min field. + </doc> + </rule> + + <rule name="no-heartbeat-min"> + <doc> + If no heartbeat-min is supplied, then the heartbeat-max field MUST remain empty. + </doc> + </rule> + </field> + </control> + + <!-- - Control: connection.tune-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="tune-ok" code="0x6" label="negotiate connection tuning parameters"> + <doc> + This control sends the client's connection tuning parameters to the server. Certain fields + are negotiated, others provide capability information. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="channel-max" type="uint16" label="negotiated maximum channels" required="true"> + <doc> + The maximum total number of channels that the client will use per connection. + </doc> + + <rule name="upper-limit"> + <doc> + If the client specifies a channel max that is higher than the value provided by the + server, the server MUST close the connection without attempting a negotiated close. The + server may report the error in some fashion to assist implementers. + </doc> + + </rule> + + <rule name="available-channels"> + <doc> + If the client agrees to a channel-max of N channels, then the channels available for + communication between client and server are precisely the channels numbered 0 to (N-1). + </doc> + </rule> + </field> + + <field name="max-frame-size" type="uint16" label="negotiated maximum frame size"> + <doc> + The largest frame size that the client and server will use for the connection. If it is + not set means that the client does not impose any specific limit but may reject very large + frames if it cannot allocate resources for them. Note that the max-frame-size limit + applies principally to content frames, where large contents can be broken into frames of + arbitrary size. + </doc> + + <rule name="minimum"> + <doc> + Until the max-frame-size has been negotiated, both peers MUST accept frames of up to + MIN-MAX-FRAME-SIZE octets large, and the minimum negotiated value for max-frame-size is + also MIN-MAX-FRAME-SIZE. + </doc> + </rule> + + <rule name="upper-limit"> + <doc> + If the client specifies a max-frame-size that is higher than the value provided by the + server, the server MUST close the connection without attempting a negotiated close. The + server may report the error in some fashion to assist implementers. + </doc> + </rule> + + <rule name="max-frame-size"> + <doc> + A peer MUST NOT send frames larger than the agreed-upon size. A peer that receives an + oversized frame MUST close the connection with the framing-error close-code. + </doc> + </rule> + </field> + + <field name="heartbeat" type="uint16" label="negotiated heartbeat delay"> + <doc> + The delay, in seconds, of the connection heartbeat chosen by the client. If it is not set + it means the client does not want a heartbeat. + </doc> + + <rule name="permitted-range"> + <doc> + The chosen heartbeat MUST be in the range supplied by the heartbeat-min and + heartbeat-max fields of connection.tune. + </doc> + </rule> + + <rule name="no-heartbeat-min"> + <doc> + The heartbeat field MUST NOT be set if the heartbeat-min field of connection.tune was + not set by the server. + </doc> + </rule> + </field> + </control> + + <!-- - Control: connection.open - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="open" code="0x7" label="open connection to virtual host"> + <doc> + This control opens a connection to a virtual host, which is a collection of resources, and + acts to separate multiple application domains within a server. The server may apply + arbitrary limits per virtual host, such as the number of each type of entity that may be + used, per connection and/or in total. + </doc> + + <implement role="server" handle="MUST" /> + + <response name="open-ok" /> + <response name="redirect" /> + + <field name="virtual-host" type="str8" label="virtual host name" required="true"> + <doc> + The name of the virtual host to work with. + </doc> + + <rule name="separation"> + <doc> + If the server supports multiple virtual hosts, it MUST enforce a full separation of + exchanges, queues, and all associated entities per virtual host. An application, + connected to a specific virtual host, MUST NOT be able to access resources of another + virtual host. + </doc> + </rule> + + <rule name="security"> + <doc> + The server SHOULD verify that the client has permission to access the specified virtual + host. + </doc> + </rule> + </field> + + <field name="capabilities" type="str16-array" label="required capabilities"> + <doc> + The client can specify zero or more capability names. The server can use this to determine + how to process the client's connection request. + </doc> + </field> + + <field name="insist" type="bit" label="insist on connecting to server"> + <doc> + In a configuration with multiple collaborating servers, the server may respond to a + connection.open control with a Connection.Redirect. The insist option tells the server + that the client is insisting on a connection to the specified server. + </doc> + <rule name="behavior"> + <doc> + When the client uses the insist option, the server MUST NOT respond with a + Connection.Redirect control. If it cannot accept the client's connection request it + should respond by closing the connection with a suitable reply code. + </doc> + </rule> + </field> + </control> + + <!-- - Control: connection.open-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="open-ok" code="0x8" label="signal that connection is ready"> + <doc> + This control signals to the client that the connection is ready for use. + </doc> + + <implement role="client" handle="MUST" /> + + <field name="known-hosts" type="amqp-host-array" label="alternate hosts which may be used in + the case of failure"> + <doc> + Specifies an array of equivalent or alternative hosts that the server knows about, which + will normally include the current server itself. Each entry in the array will be in the + form of an IP address or DNS name, optionally followed by a colon and a port number. + Clients can cache this information and use it when reconnecting to a server after a + failure. This field may be empty. + </doc> + </field> + </control> + + <!-- - Control: connection.redirect - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="redirect" code="0x9" label="redirects client to other server"> + <doc> + This control redirects the client to another server, based on the requested virtual host + and/or capabilities. + </doc> + + <rule name="usage"> + <doc> + When getting the connection.redirect control, the client SHOULD reconnect to the host + specified, and if that host is not present, to any of the hosts specified in the + known-hosts list. + </doc> + </rule> + + <implement role="client" handle="MUST" /> + + <field name="host" type="amqp-host-url" label="server to connect to" required="true"> + <doc> + Specifies the server to connect to. + </doc> + </field> + + <field name="known-hosts" type="amqp-host-array" label="alternate hosts to try in case of + failure"> + <doc> + An array of equivalent or alternative hosts that the server knows about. + </doc> + </field> + </control> + + <!-- - Control: connection.heartbeat - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="heartbeat" code="0xa" label="indicates connection is still alive"> + <doc> + The heartbeat control may be used to generate artificial network traffic when a connection + is idle. If a connection is idle for more than twice the negotiated heartbeat delay, the + peers MAY be considered disconnected. + </doc> + <implement role="client" handle="MAY" /> + <implement role="server" handle="MAY" /> + </control> + + <!-- - Control: connection.close - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="close" code="0xb" label="request a connection close"> + <doc> + This control indicates that the sender wants to close the connection. The reason for close + is indicated with the reply-code and reply-text. The channel this control is sent on MAY be + used to indicate which channel caused the connection to close. + </doc> + + <implement role="client" handle="MUST" /> + <implement role="server" handle="MUST" /> + + <response name="close-ok" /> + + <field name="reply-code" type="close-code" label="the numeric reply code" + required="true"> + <doc> + Indicates the reason for connection closure. + </doc> + </field> + <field name="reply-text" type="str8" label="the localized reply text"> + <doc> + This text can be logged as an aid to resolving issues. + </doc> + </field> + </control> + + <!-- - Control: connection.close-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <control name="close-ok" code="0xc" label="confirm a connection close"> + <doc> + This control confirms a connection.close control and tells the recipient that it is safe to + release resources for the connection and close the socket. + </doc> + + <rule name="reporting"> + <doc> + A peer that detects a socket closure without having received a Close-Ok handshake control + SHOULD log the error. + </doc> + </rule> + + <implement role="client" handle="MUST" /> + <implement role="server" handle="MUST" /> + </control> + + </class> + + <!-- == Class: session ======================================================================= --> + + <class name="session" code="0x2" label="session controls"> + <doc> + A session is a named interaction between two peers. Session names are chosen by the upper + layers and may be used indefinitely. The model layer may associate long-lived or durable state + with a given session name. The session layer provides transport of commands associated with + this interaction. + </doc> + + <doc> + The controls defined within this class are specified in terms of the "sender" of commands and + the "receiver" of commands. Since both client and server send and receive commands, the + overall session dialog is symmetric, however the semantics of the session controls are defined + in terms of a single sender/receiver pair, and it is assumed that the client and server will + each contain both a sender and receiver implementation. + </doc> + + <rule name="attachment"> + <doc> + The transport MUST be attached in order to use any control other than "attach", "attached", + "detach", or "detached". A peer receiving any other control on a detached transport MUST + discard it and send a session.detached with the "not-attached" reason code. + </doc> + </rule> + + <role name="server" implement="MUST" /> + <role name="client" implement="MUST" /> + + <role name="sender" implement="MUST"> + <doc> + The sender of commands. + </doc> + </role> + <role name="receiver" implement="MUST"> + <doc> + The receiver of commands. + </doc> + </role> + + <domain name="name" type="vbin16" label="opaque session name"> + <doc> + The session name uniquely identifies an interaction between two peers. It is scoped to a + given authentication principal. + </doc> + </domain> + + <domain name="detach-code" type="uint8" label="reason for detach"> + <enum> + <choice name="normal" value="0"> + <doc> + The session was detached by request. + </doc> + </choice> + <choice name="session-busy" value="1"> + <doc> + The session is currently attached to another transport. + </doc> + </choice> + <choice name="transport-busy" value="2"> + <doc> + The transport is currently attached to another session. + </doc> + </choice> + <choice name="not-attached" value="3"> + <doc> + The transport is not currently attached to any session. + </doc> + </choice> + <choice name="unknown-ids" value="4"> + <doc> + Command data was received prior to any use of the command-point control. + </doc> + </choice> + </enum> + </domain> + + <domain name="commands" type="sequence-set" label="identifies a set of commands"> + </domain> + + <struct name="header" size="1" pack="1"> + <doc> + The session header appears on commands after the class and command id, but prior to command + arguments. + </doc> + + <field name="sync" type="bit" label="request notification of completion"> + <doc> + Request notification of completion for this command. + </doc> + </field> + </struct> + + <struct name="command-fragment" size="0" pack="0" label="byte-ranges within a set of commands"> + + <field name="command-id" type="sequence-no" required="true"> + + </field> + <field name="byte-ranges" type="byte-ranges" required="true"> + + </field> + </struct> + + <domain name="command-fragments" type="array" label="an array of values of type + command-fragment"/> + + <control name="attach" code="0x1" label="attach to the named session"> + <doc> + Requests that the current transport be attached to the named session. Success or failure + will be indicated with an attached or detached response. This control is idempotent. + </doc> + + <rule name="one-transport-per-session"> + <doc> + A session MUST NOT be attached to more than one transport at a time. + </doc> + </rule> + + <rule name="one-session-per-transport"> + <doc> + A transport MUST NOT be attached to more than one session at a time. + </doc> + </rule> + + <rule name="idempotence"> + <doc> + Attaching a session to its current transport MUST succeed and result in an attached + response. + </doc> + </rule> + + <rule name="scoping"> + <doc> + Attachment to the same session name from distinct authentication principals MUST succeed. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MAY" /> + + <response name="attached"/> + <response name="detached"/> + + <field name="name" type="name" label="the session name" required="true"> + <doc> + Identifies the session to be attached to the current transport. + </doc> + </field> + + <field name="force" type="bit" label="force attachment to a busy session"> + <doc> + If set then a busy session will be forcibly detached from its other transport and + reattached to the current transport. + </doc> + </field> + </control> + + <control name="attached" code="0x2" label="confirm attachment to the named session"> + <doc> + Confirms successful attachment of the transport to the named session. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="name" type="name" label="the session name" required="true"> + <doc> + Identifies the session now attached to the current transport. + </doc> + </field> + </control> + + <control name="detach" code="0x3" label="detach from the named session"> + <doc> + Detaches the current transport from the named session. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <response name="detached"/> + + <field name="name" type="name" label="the session name" required="true"> + <doc> + Identifies the session to detach. + </doc> + </field> + </control> + + <control name="detached" code="0x4" label="confirm detachment from the named session"> + <doc> + Confirms detachment of the current transport from the named session. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="name" type="name" label="the session name" required="true"> + <doc> + Identifies the detached session. + </doc> + </field> + <field name="code" type="detach-code" label="the reason for detach" required="true"> + <doc> + Identifies the reason for detaching from the named session. + </doc> + </field> + </control> + + <!-- + Execution state is the set of confirmed, and completed incoming commands, as well as the set + of outgoing in-doubt commands held for replay. + --> + + <control name="request-timeout" code="0x5" label="requests the execution timeout be changed"> + <doc> + This control may be sent by either the sender or receiver of commands. It requests that the + execution timeout be changed. This is the minimum amount of time that a peer must preserve + execution state for a detached session. + </doc> + + <rule name="maximum-granted-timeout"> + <doc> + The handler of this request MUST set his timeout to the maximum allowed value less than or + equal to the requested timeout, and MUST convey the chosen timeout in the response. + </doc> + </rule> + + <implement role="sender" handle="MUST" /> + <implement role="receiver" handle="MUST" /> + + <response name="timeout"/> + + <field name="timeout" type="uint32" label="the requested timeout"> + <doc> + The requested timeout for execution state in seconds. If not set, this control requests + that execution state is preserved indefinitely. + </doc> + </field> + </control> + + <control name="timeout" code="0x6" label="the granted timeout"> + <doc> + This control may be sent by the either the sender or receiver of commands. It is a + one-to-one reply to the request-timeout control that indicates the granted timeout for + execution state. + </doc> + + <implement role="sender" handle="MUST" /> + <implement role="receiver" handle="MUST" /> + + <field name="timeout" type="uint32" label="the execution timeout"> + <doc> + The timeout for execution state. If not set, then execution state is preserved + indefinitely. + </doc> + </field> + </control> + + <control name="command-point" code="0x7" + label="the command id and byte offset of subsequent data"> + <doc> + This control is sent by the sender of commands and handled by the receiver of commands. This + establishes the sequence numbers associated with all subsequent command data sent from the + sender to the receiver. The subsequent command data will be numbered starting with the + values supplied in this control and proceeding sequentially. This must be used at least once + prior to sending any command data on newly attached transports. + </doc> + + <rule name="newly-attached-transports"> + <doc> + If command data is sent on a newly attached transport the session MUST be detached with an + "unknown-id" reason-code. + </doc> + </rule> + + <rule name="zero-offset"> + <doc> + If the offset is zero, the next data frame MUST have the first-frame and first-segment + flags set. Violation of this is a framing error. + </doc> + </rule> + + <rule name="nonzero-offset"> + <doc> + If the offset is nonzero, the next data frame MUST NOT have both the first-frame and + first-segment flag set. Violation of this is a framing error. + </doc> + </rule> + + <implement role="receiver" handle="MUST" /> + + <field name="command-id" type="sequence-no" label="the command-id of the next command" + required="true"/> + <field name="command-offset" type="uint64" label="the byte offset within the command" + required="true"/> + </control> + + <control name="expected" code="0x8" label="informs the peer of expected commands"> + <doc> + This control is sent by the receiver of commands and handled by the sender of commands. It + informs the sender of what commands and command fragments are expected at the receiver. + This control is only sent in response to a flush control with the expected flag set. The + expected control is never sent spontaneously. + </doc> + + <rule name="include-next-command"> + <doc> + The set of expected commands MUST include the next command after the highest seen command. + </doc> + </rule> + + <rule name="commands-empty-means-new-session"> + <doc> + The set of expected commands MUST have zero elements if and only if the sender holds no + execution state for the session (i.e. it is a new session). + </doc> + </rule> + + <rule name="no-overlaps"> + <doc> + If a command-id appears in the commands field, it MUST NOT appear in the fragments field. + </doc> + </rule> + + <rule name="minimal-fragments"> + <doc> + When choice is permitted, a command MUST appear in the commands field rather than the + fragments field. + </doc> + </rule> + + <implement role="sender" handle="MUST" /> + + <field name="commands" type="commands" label="expected commands" required="true"/> + <field name="fragments" type="command-fragments" label="expected fragments" /> + </control> + + <control name="confirmed" code="0x9" label="notifies of confirmed commands"> + <doc> + This control is sent by the receiver of commands and handled by the sender of commands. This + sends the set of commands that will definitely be completed by this peer to the sender. This + excludes commands known by the receiver to be considered confirmed or complete at the + sender. + </doc> + <doc> + This control must be sent if the partner requests the set of confirmed commands using the + session.flush control with the confirmed flag set. + </doc> + <doc> + This control may be sent spontaneously. One reason for separating confirmation from + completion is for large persistent messages, where the receipt (and storage to a durable + store) of part of the message will result in less data needing to be replayed in the case of + transport failure during transmission. + </doc> + <doc> + A simple implementation of an AMQP client or server may be implemented to take no action on + receipt of session.confirmed controls, and take action only when receiving + session.completed controls. + </doc> + <doc> + A simple implementation of an AMQP client or server may be implemented such that it never + spontaneously sends session.confirmed and that when requested for the set of confirmed + commands (via the session.flush control) it responds with the same set of commands as it + would to when the set of completed commands was requested (trivially all completed commands + are confirmed). + </doc> + + <rule name="durability"> + <doc> + If a command has durable implications, it MUST NOT be confirmed until the fact of the + command has been recorded on durable media. + </doc> + </rule> + + <rule name="no-overlaps"> + <doc> + If a command-id appears in the commands field, it MUST NOT appear in the fragments field. + </doc> + </rule> + + <rule name="minimal-fragments"> + <doc> + When choice is permitted, a command MUST appear in the commands field rather than the + fragments field. + </doc> + </rule> + + <implement role="sender" handle="MUST" /> + + <field name="commands" type="commands" label="entirely confirmed commands"> + <rule name="exclude-known-complete"> + <doc> + Command-ids included in prior known-complete replies MUST be excluded from the set of + all confirmed commands. + </doc> + </rule> + </field> + <field name="fragments" type="command-fragments" label="partially confirmed commands"/> + </control> + + <control name="completed" code="0xa" label="notifies of command completion"> + <doc> + This control is sent by the receiver of commands, and handled by the sender of commands. It + informs the sender of all commands completed by the receiver. This excludes commands known + by the receiver to be considered complete at the sender. + </doc> + + <rule name="known-completed-reply"> + <doc> + The sender MUST eventually reply with a known-completed set that covers the completed ids. + </doc> + </rule> + + <rule name="delayed-reply"> + <doc> + The known-complete reply MAY be delayed at the senders discretion if the timely-reply + field is not set. + </doc> + </rule> + + <rule name="merged-reply"> + <doc> + Multiple replies may be merged by sending a single known-completed that includes the union + of the merged command-id sets. + </doc> + </rule> + + <implement role="sender" handle="MUST" /> + + <field name="commands" type="commands" label="completed commands"> + <doc> + The ids of all completed commands. This excludes commands known by the receiver to be + considered complete at the sender. + </doc> + + <rule name="completed-implies-confirmed"> + <doc> + The sender MUST consider any completed commands to also be confirmed. + </doc> + </rule> + + <rule name="exclude-known-complete"> + <doc> + Command-ids included in prior known-complete replies MUST be excluded from the set of + all completed commands. + </doc> + </rule> + </field> + <field name="timely-reply" type="bit"> + <doc> + If set, the sender is no longer free to delay the known-completed reply. + </doc> + </field> + </control> + + <control name="known-completed" code="0xb" label="Inform peer of which commands are known to be + completed"> + <doc> + This control is sent by the sender of commands, and handled by the receiver of commands. It + is sent in reply to one or more completed controls from the receiver. It informs the + receiver that commands are known to be completed by the sender. + </doc> + + <rule name="stateless"> + <doc> + The sender need not keep state to generate this reply. It is sufficient to reply to any + completed control with an exact echo of the completed ids. + </doc> + </rule> + + <implement role="receiver" handle="MUST" /> + + <field name="commands" type="commands" label="commands known to be complete"> + <doc> + The set of completed commands for one or more session.completed controls. + </doc> + + <rule name="known-completed-implies-known-confirmed"> + <doc> + The receiver MUST treat any of the specified commands to be considered by the sender as + confirmed as well as completed. + </doc> + </rule> + </field> + </control> + + <control name="flush" code="0xc" label="requests a session.completed"> + <doc> + This control is sent by the sender of commands and handled by the receiver of commands. It + requests that the receiver produce the indicated command sets. The receiver should issue the + indicated sets at the earliest possible opportunity. + </doc> + + <implement role="receiver" handle="MUST" /> + + <field name="expected" type="bit" label="request notification of expected commands"/> + <field name="confirmed" type="bit" label="request notification of confirmed commands"/> + <field name="completed" type="bit" label="request notification of completed commands"/> + </control> + + <control name="gap" code="0xd" label="indicates missing segments in the stream"> + <doc> + This control is sent by the sender of commands and handled by the receiver of commands. It + sends command ranges for which there will be no further data forthcoming. The receiver + should proceed with the next available commands that arrive after the gap. + </doc> + + <rule name="gap-confirmation-and-completion"> + <doc> + The command-ids covered by a session.gap MUST be added to the completed and confirmed sets + by the receiver. + </doc> + </rule> + + <rule name="aborted-commands"> + <doc> + If a session.gap covers a partially received command, the receiving peer MUST treat the + command as aborted. + </doc> + </rule> + + <rule name="completed-or-confirmed-commands"> + <doc> + If a session.gap covers a completed or confirmed command, the receiving peer MUST continue + to treat the command as completed or confirmed. + </doc> + </rule> + + <implement role="receiver" handle="MUST" /> + + <field name="commands" type="commands"> + <doc> + The set of command-ids that are contained in this gap. + </doc> + </field> + </control> + + </class> + + <!-- == Class: execution ===================================================================== --> + + <class name="execution" code="0x3" label="execution commands"> + <doc> + The execution class provides commands that carry execution information about other model level + commands. + </doc> + + <role name="server" implement="MUST"/> + <role name="client" implement="MUST"/> + + <domain name="error-code" type="uint16"> + <enum> + <choice name="unauthorized-access" value="403"> + <doc> + The client attempted to work with a server entity to which it has no access due to + security settings. + </doc> + </choice> + + <choice name="not-found" value="404"> + <doc> + The client attempted to work with a server entity that does not exist. + </doc> + </choice> + + <choice name="resource-locked" value="405"> + <doc> + The client attempted to work with a server entity to which it has no access because + another client is working with it. + </doc> + </choice> + + <choice name="precondition-failed" value="406"> + <doc> + The client requested a command that was not allowed because some precondition failed. + </doc> + </choice> + + <choice name="resource-deleted" value="408"> + <doc> + A server entity the client is working with has been deleted. + </doc> + </choice> + + <choice name="illegal-state" value="409"> + <doc> + The peer sent a command that is not permitted in the current state of the session. + </doc> + </choice> + + <choice name="command-invalid" value="503"> + <doc> + The command segments could not be decoded. + </doc> + </choice> + + <choice name="resource-limit-exceeded" value="506"> + <doc> + The client exceeded its resource allocation. + </doc> + </choice> + + <choice name="not-allowed" value="530"> + <doc> + The peer tried to use a command a manner that is inconsistent with the rules described + in the specification. + </doc> + </choice> + + <choice name="illegal-argument" value="531"> + <doc> + The command argument is malformed, i.e. it does not fall within the specified domain. + The illegal-argument exception can be raised on execution of any command which has + domain valued fields. + </doc> + </choice> + + <choice name="not-implemented" value="540"> + <doc> + The peer tried to use functionality that is not implemented in its partner. + </doc> + </choice> + + <choice name="internal-error" value="541"> + <doc> + The peer could not complete the command because of an internal error. The peer may + require intervention by an operator in order to resume normal operations. + </doc> + </choice> + + <choice name="invalid-argument" value="542"> + <doc> + An invalid argument was passed to a command, and the operation could not + proceed. An invalid argument is not illegal (see illegal-argument), i.e. it matches + the domain definition; however the particular value is invalid in this context. + </doc> + </choice> + </enum> + </domain> + + <!-- - Command: execution.sync - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="sync" code="0x1" label="request notification of completion for issued commands"> + <doc> + This command is complete when all prior commands are completed. + </doc> + + <implement role="server" handle="MUST"/> + <implement role="client" handle="MUST"/> + </command> + + <!-- - Command: execution.result - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="result" code="0x2" label="carries execution results"> + <doc> + This command carries data resulting from the execution of a command. + </doc> + + <implement role="server" handle="MUST"/> + <implement role="client" handle="MUST"/> + + <field name="command-id" type="sequence-no" required="true"/> + <field name="value" type="struct32"/> + </command> + + <!-- - Command: execution.exception - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="exception" code="0x3" label="notifies a peer of an execution error"> + <doc> + This command informs a peer of an execution exception. The command-id, when given, + correlates the error to a specific command. + </doc> + + <implement role="client" handle="MUST"/> + <implement role="server" handle="MUST"/> + + <field name="error-code" type="error-code" required="true" label="error code indicating the + type of error"/> + <field name="command-id" type="sequence-no" label="exceptional command"> + <doc> + The command-id of the command which caused the exception. If the exception was not caused + by a specific command, this value is not set. + </doc> + </field> + <field name="class-code" type="uint8" label="the class code of the command whose execution + gave rise to the error (if appropriate)"/> + <field name="command-code" type="uint8" label="the class code of the command whose execution + gave rise to the error (if appropriate)"/> + <field name="field-index" type="uint8" label="index of the exceptional field"> + <doc> + The zero based index of the exceptional field within the arguments to the exceptional + command. If the exception was not caused by a specific field, this value is not set. + </doc> + </field> + <field name="description" type="str16" label="descriptive text on the exception"> + <doc> + The description provided is implementation defined, but MUST be in the language + appropriate for the selected locale. The intention is that this description is suitable + for logging or alerting output. + </doc> + </field> + <field name="error-info" type="map" label="map to carry additional information about the + error"/> + + </command> + + </class> + + <!-- == Class: message ======================================================================= --> + + <class name="message" code="0x4" label="message transfer"> + <doc> + The message class provides commands that support an industry-standard messaging model. + </doc> + + <doc type="picture" title="Transfer States"> + START: + + The message has yet to be sent to the recipient. + + NOT-ACQUIRED: + + The message has been sent to the recipient, but is not + acquired by the recipient. + + ACQUIRED: + + The message has been sent to and acquired by the recipient. + + END: + + The transfer is complete. + </doc> + + <doc type="picture" title="State Transitions"><![CDATA[ + *:TRANSFER (accept-mode=none) *:TRANSFER (acquire-mode=pre-acquired) + +---------------------------------START------------------------------------------+ + | | | + | | *:TRANSFER (acquire-mode=not-acquired) | + | | | + | R:RELEASE \|/ | + | +-------------NOT-ACQUIRED<--+ | + | | | | | R:ACQUIRE (if unavailable) | + | | | +-----+ | + | | | | + | | | R:ACQUIRE (if available) | + | | | | + | | \|/ | + | | ACQUIRED<-------------------------------------------+ + | | | + | | | R:ACCEPT / R:REJECT / R:RELEASE + | | | + | | \|/ + | +------------->END]]> + | /|\ + | | + +-------------------------------+ + </doc> + + <doc type="grammar"> + message = *:TRANSFER [ R:ACQUIRE ] [ R:ACCEPT / R:REJECT / R:RELEASE ] + / *:RESUME + / *:SET-FLOW-MODE + / *:FLOW + / *:STOP + / C:SUBSCRIBE + / C:CANCEL + / C:FLUSH + </doc> + + <rule name="persistent-message"> + <doc> + The server SHOULD respect the delivery-mode property of messages and SHOULD make a + best-effort to hold persistent messages on a reliable storage mechanism. + </doc> + <doc type="scenario"> + Send a persistent message to queue, stop server, restart server and then verify whether + message is still present. Assumes that queues are durable. Persistence without durable + queues makes no sense. + </doc> + </rule> + + <rule name="no-persistent-message-discard"> + <doc> + The server MUST NOT discard a persistent message in case of a queue overflow. + </doc> + <doc type="scenario"> + Create a queue overflow situation with persistent messages and verify that messages do not + get lost (presumably the server will write them to disk). + </doc> + </rule> + + <rule name="throttling"> + <doc> + The server MAY use the message.flow command to slow or stop a message publisher when + necessary. + </doc> + </rule> + + <rule name="non-persistent-message-overflow"> + <doc> + The server MAY overflow non-persistent messages to persistent storage. + </doc> + </rule> + + <rule name="non-persistent-message-discard"> + <doc> + The server MAY discard or dead-letter non-persistent messages on a priority basis if the + queue size exceeds some configured limit. + </doc> + </rule> + + <rule name="min-priority-levels"> + <doc> + The server MUST implement at least 2 priority levels for messages, where priorities 0 and + 9 are treated as two distinct levels. + </doc> + </rule> + + <rule name="priority-level-implementation"> + <doc> + The server SHOULD implement distinct priority levels in the following manner: + </doc> + <doc> + If the server implements n distinct priorities then priorities 0 to 5 - ceiling(n/2) should + be treated equivalently and should be the lowest effective priority. The priorities 4 + + floor(n/2) should be treated equivalently and should be the highest effective priority. The + priorities (5 - ceiling(n/2)) to (4 + floor(n/2)) inclusive must be treated as distinct + priorities. + </doc> + <doc> + Thus, for example, if 2 distinct priorities are implemented, then levels 0 to 4 are + equivalent, and levels 5 to 9 are equivalent and levels 4 and 5 are distinct. If 3 distinct + priorities are implements the 0 to 3 are equivalent, 5 to 9 are equivalent and 3, 4 and 5 + are distinct. + </doc> + <doc> + This scheme ensures that if two priorities are distinct for a server which implements m + separate priority levels they are also distinct for a server which implements n different + priority levels where n > m. + </doc> + </rule> + + <rule name="priority-delivery"> + <doc> + The server MUST deliver messages of the same priority in order irrespective of their + individual persistence. + </doc> + <doc type="scenario"> + Send a set of messages with the same priority but different persistence settings to a queue. + Subscribe and verify that messages arrive in same order as originally published. + </doc> + </rule> + + <role name="server" implement="MUST" /> + <role name="client" implement="MUST" /> + + <domain name="destination" type="str8" label="destination for a message"> + <doc> + Specifies the destination to which the message is to be transferred. + </doc> + </domain> + + <domain name="accept-mode" type="uint8" label="indicates a confirmation mode"> + <doc> + Controls how the sender of messages is notified of successful transfer. + </doc> + + <enum> + <choice name="explicit" value="0"> + <doc> + Successful transfer is signaled by message.accept. An acquired message (whether + acquisition was implicit as in pre-acquired mode or explicit as in not-acquired mode) is + not considered transferred until a message.accept that includes the transfer command is + received. + </doc> + </choice> + + <choice name="none" value="1"> + <doc> + Successful transfer is assumed when accept-mode is "pre-acquired". Messages transferred + with an accept-mode of "not-acquired" cannot be acquired when accept-mode is "none". + </doc> + </choice> + </enum> + </domain> + + <domain name="acquire-mode" type="uint8" label="indicates the transfer mode"> + <doc> + Indicates whether a transferred message can be considered as automatically acquired or + whether an explicit request is necessary in order to acquire it. + </doc> + + <enum> + <choice name="pre-acquired" value="0"> + <doc> + the message is acquired when the transfer starts + </doc> + </choice> + + <choice name="not-acquired" value="1"> + <doc> + the message is not acquired when it arrives, and must be explicitly acquired by the + recipient + </doc> + </choice> + </enum> + </domain> + + <domain name="reject-code" type="uint16" label="reject code for transfer"> + <doc> + Code specifying the reason for a message reject. + </doc> + <enum> + <choice name="unspecified" value="0"> + <doc> + Rejected for an unspecified reason. + </doc> + </choice> + <choice name="unroutable" value="1"> + <doc> + Delivery was attempted but there were no queues which the message could be routed to. + </doc> + </choice> + <choice name="immediate" value="2"> + <doc> + The rejected message had the immediate flag set to true, but at the time of the transfer + at least one of the queues to which it was to be routed did not have any subscriber able + to take the message. + </doc> + </choice> + </enum> + </domain> + + <domain name="resume-id" type="str16"> + <doc> + A resume-id serves to identify partially transferred message content. The id is chosen by + the sender, and must be unique to a given user. A resume-id is not expected to be unique + across users. + </doc> + </domain> + + <domain name="delivery-mode" type="uint8" + label="indicates whether a message should be treated as transient or durable"> + <doc> + + Used to set the reliability requirements for a message which is transferred to the server. + </doc> + <enum> + <choice name="non-persistent" value="1"> + <doc> + A non-persistent message may be lost in event of a failure, but the nature of the + communication is such that an occasional message loss is tolerable. This is the lowest + overhead mode. Non-persistent messages are delivered at most once only. + </doc> + </choice> + + <choice name="persistent" value="2"> + <doc> + A persistent message is one which must be stored on a persistent medium (usually hard + drive) at every stage of delivery so that it will not be lost in event of failure (other + than of the medium itself). This is normally accomplished with some additional overhead. + A persistent message may be delivered more than once if there is uncertainty about the + state of its delivery after a failure and recovery. + </doc> + </choice> + </enum> + </domain> + + <domain name="delivery-priority" type="uint8" + label="indicates the desired priority to assign to a message transfer"> + <doc> + Used to assign a priority to a message transfer. Priorities range from 0 (lowest) to 9 + (highest). + </doc> + <enum> + <choice name="lowest" value="0"> + <doc> + Lowest possible priority message. + </doc> + </choice> + + <choice name="lower" value="1"> + <doc> + Very low priority message + </doc> + </choice> + + <choice name="low" value="2"> + <doc> + Low priority message. + </doc> + </choice> + + <choice name="below-average" value="3"> + <doc> + Below average priority message. + </doc> + </choice> + + <choice name="medium" value="4"> + <doc> + Medium priority message. + </doc> + </choice> + + + <choice name="above-average" value="5"> + <doc> + Above average priority message + </doc> + </choice> + + + <choice name="high" value="6"> + <doc> + High priority message + </doc> + </choice> + + <choice name="higher" value="7"> + <doc> + Higher priority message + </doc> + </choice> + + <choice name="very-high" value="8"> + <doc> + Very high priority message. + </doc> + </choice> + + <choice name="highest" value="9"> + <doc> + Highest possible priority message. + </doc> + </choice> + </enum> + </domain> + + <struct name="delivery-properties" size="4" code="0x1" pack="2"> + <field name="discard-unroutable" type="bit" label="controls discard of unroutable messages"> + <doc> + If set on a message that is not routable the broker can discard it. If not set, an + unroutable message should be handled by reject when accept-mode is explicit; or by routing + to the alternate-exchange if defined when accept-mode is none. + </doc> + </field> + + <field name="immediate" type="bit" label="Consider message unroutable if it cannot be + processed immediately"> + <doc> + If the immediate flag is set to true on a message transferred to a Server, then the + message should be considered unroutable (and not delivered to any queues) if, for any + queue that it is to be routed to according to the standard routing behavior, there is not + a subscription on that queue able to receive the message. The treatment of unroutable + messages is dependent on the value of the discard-unroutable flag. + </doc> + <doc> + The immediate flag is ignored on transferred to a Client. + </doc> + </field> + + <field name="redelivered" type="bit" label="redelivery flag"> + <doc> + This boolean flag indicates that the message may have been previously delivered to this + or another client. + </doc> + <doc> + If the redelivered flag is set on transfer to a Server, then any delivery of the message + from that Server to a Client must also have the redelivered flag set to true. + </doc> + <rule name="implementation"> + <doc> + The server MUST try to signal redelivered messages when it can. When redelivering a + message that was not successfully accepted, the server SHOULD deliver it to the original + client if possible. + </doc> + <doc type="scenario"> + Create a shared queue and publish a message to the queue. Subscribe using explicit + accept-mode, but do not accept the message. Close the session, reconnect, and subscribe + to the queue again. The message MUST arrive with the redelivered flag set. + </doc> + </rule> + <rule name="hinting"> + <doc> + The client should not rely on the redelivered field to detect duplicate messages where + publishers may themselves produce duplicates. A fully robust client should be able to + track duplicate received messages on non-transacted, and locally-transacted sessions. + </doc> + </rule> + </field> + + <field name="priority" type="delivery-priority" label="message priority, 0 to 9" + required="true"> + <doc> Message priority, which can be between 0 and 9. Messages with higher priorities may be + delivered before those with lower priorities. </doc> + </field> + + <field name="delivery-mode" type="delivery-mode" label="message persistence requirement" + required="true"> + <doc> The delivery mode may be non-persistent or persistent. </doc> + </field> + + <field name="ttl" type="uint64" label="time to live in ms"> + <doc> Duration in milliseconds for which the message should be considered "live". If this is + set then a message expiration time will be computed based on the current time plus this + value. Messages that live longer than their expiration time will be discarded (or dead + lettered).</doc> + <rule name="ttl-decrement"> + <doc> + If a message is transferred between brokers before delivery to a final subscriber the + ttl should be decremented before peer to peer transfer and both timestamp and expiration + should be cleared. + </doc> + </rule> + </field> + + <field name="timestamp" type="datetime" label="message timestamp"> + <doc> + The timestamp is set by the broker on arrival of the message. + </doc> + </field> + + <field name="expiration" type="datetime" label="message expiration time"> + <doc> + The expiration header assigned by the broker. After receiving the message the broker sets + expiration to the sum of the ttl specified in the publish command and the current time. + (ttl=expiration - timestamp) + </doc> + </field> + + <field name="exchange" type="exchange.name" label="originating exchange"> + <doc> + Identifies the exchange specified in the destination field of the message.transfer used to + publish the message. This MUST be set by the broker upon receipt of a message. + </doc> + </field> + + <field name="routing-key" type="str8" label="message routing key"> + <doc> + The value of the key determines to which queue the exchange will send the message. The way + in which keys are used to make this routing decision depends on the type of exchange to + which the message is sent. For example, a direct exchange will route a message to a queue + if that queue is bound to the exchange with a binding-key identical to the routing-key of + the message. + </doc> + </field> + + <field name="resume-id" type="resume-id" label="global id for message transfer"> + <doc> + When a resume-id is provided the recipient MAY use it to retain message data should the + session expire while the message transfer is still incomplete. + </doc> + </field> + + <field name="resume-ttl" type="uint64" label="ttl in ms for interrupted message data"> + <doc> + When a resume-ttl is provided the recipient MAY use it has a guideline for how long to + retain the partially complete data when a resume-id is specified. If no resume-id is + specified then this value should be ignored. + </doc> + </field> + </struct> + + <struct name="fragment-properties" size="4" code="0x2" pack="2"> + <doc> + These properties permit the transfer of message fragments. These may be used in conjunction + with byte level flow control to limit the rate at which large messages are received. Only + the first fragment carries the delivery-properties and message-properties. + + Syntactically each fragment appears as a complete message to the lower layers of the + protocol, however the model layer is required to treat all the fragments as a single + message. For example all fragments must be delivered to the same client. In pre-acquired + mode, no message fragments can be delivered by the broker until the entire message has been + received. + </doc> + + <field name="first" type="bit" default="1"> + <doc>True if this fragment contains the start of the message, false otherwise.</doc> + </field> + + <field name="last" type="bit" default="1"> + <doc>True if this fragment contains the end of the message, false otherwise.</doc> + </field> + + <field name="fragment-size" type="uint64"> + <doc>This field may optionally contain the size of the fragment.</doc> + </field> + </struct> + + <struct name="reply-to" size="2" pack="2"> + <doc>The reply-to domain provides a simple address structure for replying to to a message to a + destination within the same virtual-host.</doc> + <field name="exchange" type="exchange.name" label="the name of the exchange to reply to"/> + <field name="routing-key" type="str8" label="the routing-key to use when replying"/> + </struct> + + <struct name="message-properties" size="4" code="0x3" pack="2"> + <field name="content-length" type="uint64" label="length of the body segment in bytes"> + <doc> + The length of the body segment in bytes. + </doc> + </field> + + <field name="message-id" type="uuid" label="application message identifier"> + <doc> + Message-id is an optional property of UUID type which uniquely identifies a message within + the message system. The message producer is usually responsible for setting the + message-id. The server MAY discard a message as a duplicate if the value of the message-id + matches that of a previously received message. Duplicate messages MUST still be accepted + if transferred with an accept-mode of "explicit". + </doc> + + <rule name="unique"> + <doc> + A message-id MUST be unique within a given server instance. A message-id SHOULD be + globally unique (i.e. across different systems). + </doc> + </rule> + + <rule name="immutable"> + <doc> + A message ID is immutable. Once set, a message-id MUST NOT be changed or reassigned, + even if the message is replicated, resent or sent to multiple queues. + </doc> + </rule> + </field> + + <field name="correlation-id" type="vbin16" label="application correlation identifier"> + <doc> + This is a client-specific id that may be used to mark or identify messages between + clients. The server ignores this field. + </doc> + </field> + + <field name="reply-to" type="reply-to" label="destination to reply to"> + <doc> + The destination of any message that is sent in reply to this message. + </doc> + </field> + + <field name="content-type" type="str8" label="MIME content type"> + <doc> + The RFC-2046 MIME type for the message content (such as "text/plain"). This is set by the + originating client. + </doc> + </field> + + <field name="content-encoding" type="str8" label="MIME content encoding"> + <doc> + The encoding for character-based message content. This is set by the originating client. + Examples include UTF-8 and ISO-8859-15. + </doc> + </field> + + <field name="user-id" type="vbin16" label="creating user id"> + <doc> + The identity of the user responsible for producing the message. The client sets this + value, and it is authenticated by the broker. + </doc> + + <rule name="authentication"> + <doc> + The server MUST produce an unauthorized-access exception if the user-id field is set to + a principle for which the client is not authenticated. + </doc> + </rule> + </field> + + <field name="app-id" type="vbin16" label="creating application id"> + <doc> + The identity of the client application responsible for producing the message. + </doc> + </field> + + <field name="application-headers" type="map" label="application specific headers table"> + <doc> + This is a collection of user-defined headers or properties which may be set by the + producing client and retrieved by the consuming client. + </doc> + </field> + </struct> + + <domain name="flow-mode" type="uint8" label="the flow-mode for allocating flow credit"> + <enum> + <choice name="credit" value="0"> + <doc> + Credit based flow control. + </doc> + </choice> + + <choice name="window" value="1"> + <doc> + Window based flow control. + </doc> + </choice> + </enum> + </domain> + + <domain name="credit-unit" type="uint8" label="specifies the unit of credit balance"> + <enum> + <choice name="message" value="0"> + <doc>Indicates a value specified in messages.</doc> + </choice> + <choice name="byte" value="1"> + <doc>Indicates a value specified in bytes.</doc> + </choice> + </enum> + </domain> + + <!-- - Command: message.transfer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="transfer" code="0x1" label="transfer a message"> + <doc> + This command transfers a message between two peers. When a client uses this command to + publish a message to a broker, the destination identifies a specific exchange. The message + will then be routed to queues as defined by the exchange configuration. + + The client may request a broker to transfer messages to it, from a particular queue, by + issuing a subscribe command. The subscribe command specifies the destination that the broker + should use for any resulting transfers. + </doc> + + <rule name="transactional-publish"> + <doc> + If a transfer to an exchange occurs within a transaction, then it is not available from + the queue until the transaction commits. It is not specified whether routing takes place + when the transfer is received or when the transaction commits. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + + <field name="destination" type="destination" label="message destination"> + <doc> + Specifies the destination to which the message is to be transferred. + </doc> + + <rule name="blank-destination"> + <doc> + The server MUST accept a blank destination to mean the default exchange. + </doc> + </rule> + + <exception name="nonexistent-exchange" error-code="not-found"> + <doc> + If the destination refers to an exchange that does not exist, the peer MUST raise a + session exception. + </doc> + </exception> + </field> + + <field name="accept-mode" type="accept-mode" required="true"> + <doc> + Indicates whether message.accept, session.complete, or nothing at all is required to + indicate successful transfer of the message. + </doc> + </field> + + <field name="acquire-mode" type="acquire-mode" required="true"> + <doc> + Indicates whether or not the transferred message has been acquired. + </doc> + </field> + + <segments> + <header> + <entry type="delivery-properties"/> + <entry type="fragment-properties"/> + <entry type="message-properties"/> + </header> + <body/> + </segments> + </command> + + <!-- - Command: message.accept - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="accept" code="0x2" label="reject a message"> + <doc> + Accepts the message. Once a transfer is accepted, the command-id may no longer be referenced + from other commands. + </doc> + + <rule name="acquisition"> + <doc> + The recipient MUST have acquired a message in order to accept it. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="transfers" type="session.commands" required="true"> + <doc> + Identifies the messages previously transferred that should be accepted. + </doc> + </field> + </command> + + <!-- - Command: message.reject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="reject" code="0x3" label="reject a message"> + <doc> + Indicates that the message transfers are unprocessable in some way. A server may reject a + message if it is unroutable. A client may reject a message if it is invalid. A message may + be rejected for other reasons as well. Once a transfer is rejected, the command-id may no + longer be referenced from other commands. + </doc> + + <rule name="alternate-exchange"> + <doc> + When a client rejects a message, the server MUST deliver that message to the + alternate-exchange on the queue from which it was delivered. If no alternate-exchange is + defined for that queue the broker MAY discard the message. + </doc> + </rule> + + <rule name="acquisition"> + <doc> + The recipient MUST have acquired a message in order to reject it. If the message is not + acquired any reject MUST be ignored. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="transfers" type="session.commands" required="true"> + <doc> + Identifies the messages previously transferred that should be rejected. + </doc> + </field> + <field name="code" type="reject-code" required="true"> + <doc> + Code describing the reason for rejection. + </doc> + </field> + <field name="text" type="str8" label="informational text for message reject"> + <doc> + Text describing the reason for rejection. + </doc> + </field> + </command> + + <!-- - Command: message.release - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="release" code="0x4" label="release a message"> + <doc> + Release previously transferred messages. When acquired messages are released, they become + available for acquisition by any subscriber. Once a transfer is released, the command-id may + no longer be referenced from other commands. + </doc> + + <rule name="ordering"> + <doc> + Acquired messages that have been released MAY subsequently be delivered out of order. + Implementations SHOULD ensure that released messages keep their position with respect to + undelivered messages of the same priority. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MAY" /> + + <field name="transfers" type="session.commands" required="true"> + <doc> + Indicates the messages to be released. + </doc> + </field> + <field name="set-redelivered" type="bit" label="mark the released messages as redelivered"> + <doc> + By setting set-redelivered to true, any acquired messages released to a queue with this + command will be marked as redelivered on their next transfer from that queue. If this flag + is not set, then an acquired message will retain its original redelivered status on the + queue. Messages that are not acquired are unaffected by the value of this flag. + </doc> + </field> + </command> + + <!-- - Command: message.acquire - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="acquire" code="0x5" label="acquire messages for consumption"> + <doc> + Acquires previously transferred messages for consumption. The acquired ids (if any) are + sent via message.acquired. + </doc> + + <rule name="one-to-one"> + <doc> + Each acquire MUST produce exactly one message.acquired even if it is empty. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <field name="transfers" type="session.commands" required="true"> + <doc> + Indicates the messages to be acquired. + </doc> + </field> + + <result> + <struct name="acquired" size="4" code="0x4" pack="2" label="indicates acquired messages"> + <doc> + Identifies a set of previously transferred messages that have now been acquired. + </doc> + + <field name="transfers" type="session.commands" required="true"> + <doc> + Indicates the acquired messages. + </doc> + </field> + </struct> + </result> + </command> + + <!-- - Command: message.resume - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="resume" code="0x6" label="resume an interrupted message transfer"> + <doc> + This command resumes an interrupted transfer. The recipient should return the amount of + partially transferred data associated with the given resume-id, or zero if there is no data + at all. If a non-zero result is returned, the recipient should expect to receive message + fragment(s) containing the remainder of the interrupted message. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="destination" type="destination"> + <doc> + The destination to which the remaining message fragments are transferred. + </doc> + + <exception name="destination-not-found" error-code="not-found"> + <doc>If the destination does not exist, the recipient MUST close the session.</doc> + </exception> + </field> + + <field name="resume-id" type="resume-id" required="true"> + <doc> + The name of the transfer being resumed. + </doc> + + <rule name="unknown-resume-id"> + <doc>If the resume-id is not known, the recipient MUST return an offset of zero.</doc> + </rule> + </field> + + <result> + <struct name="message-resume-result" size="4" code="0x5" pack="2"> + <field name="offset" type="uint64"> + <doc> + Indicates the amount of data already transferred. + </doc> + </field> + </struct> + </result> + </command> + + <!-- - Command: message.subscribe - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="subscribe" code="0x7" label="start a queue subscription"> + <doc> This command asks the server to start a "subscription", which is a request for messages + from a specific queue. Subscriptions last as long as the session they were created on, or + until the client cancels them. </doc> + + <rule name="simultaneous-subscriptions"> + <doc> The server SHOULD support at least 16 subscriptions per queue, and ideally, impose no + limit except as defined by available resources. </doc> + <doc type="scenario"> Create a queue and create subscriptions on that queue until the server + closes the connection. Verify that the number of subscriptions created was at least + sixteen and report the total number. </doc> + </rule> + + <rule name="default-flow-mode"> + <doc> The default flow mode for new subscriptions is window-mode. </doc> + </rule> + + <exception name="queue-deletion" error-code="resource-deleted"> + <doc> + If the queue for this subscription is deleted, any subscribing sessions MUST be closed. + This exception may occur at any time after the subscription has been completed. + </doc> + </exception> + + <exception name="queue-not-found" error-code="not-found"> + <doc> If the queue for this subscription does not exist, then the subscribing session MUST + be closed. </doc> + </exception> + + <rule name="initial-credit"> + <doc> + Immediately after a subscription is created, the initial byte and message credit for that + destination is zero. + </doc> + </rule> + + <implement role="server" handle="MUST"/> + + <field name="queue" type="queue.name" required="true"> + <doc> Specifies the name of the subscribed queue. </doc> + </field> + + <field name="destination" type="destination" label="incoming message destination"> + <doc> The client specified name for the subscription. This is used as the destination for + all messages transferred from this subscription. The destination is scoped to the session. + </doc> + + <exception name="unique-subscriber-destination" error-code="not-allowed"> + <doc> The client MUST NOT specify a destination that refers to an existing subscription on + the same session. </doc> + <doc type="scenario"> Attempt to create two subscriptions on the same session with the + same non-empty destination. </doc> + </exception> + </field> + + <field name="accept-mode" type="accept-mode" required="true"> + <doc> The accept-mode to use for messages transferred from this subscription. </doc> + </field> + + <field name="acquire-mode" type="acquire-mode" required="true"> + <doc> The acquire-mode to use for messages transferred from this subscription. </doc> + </field> + + <field name="exclusive" type="bit" label="request exclusive access"> + <doc> Request an exclusive subscription. This prevents other subscribers from subscribing to + the queue. </doc> + + <exception name="in-use" error-code="resource-locked"> + <doc> The server MUST NOT grant an exclusive subscription to a queue that already has + subscribers. </doc> + <doc type="scenario"> Open two connections to a server, and in one connection create a + shared (non-exclusive) queue and then subscribe to the queue. In the second connection + attempt to subscribe to the same queue using the exclusive option. </doc> + </exception> + </field> + + <field name="resume-id" type="resume-id"> + <doc> Requests that the broker use the supplied resume-id when transferring messages for + this subscription. </doc> + </field> + + <field name="resume-ttl" type="uint64"> + <doc> Requested duration in milliseconds for the broker use as resume-ttl when transferring + messages for this subscription. </doc> + </field> + + <field name="arguments" type="map" label="arguments for vendor extensions"> + <doc> The syntax and semantics of these arguments depends on the providers implementation. + </doc> + </field> + </command> + + <!-- - Command: message.cancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="cancel" code="0x8" label="end a queue subscription"> + <doc> + This command cancels a subscription. This does not affect already delivered messages, but it + does mean the server will not send any more messages for that subscription. The client may + receive an arbitrary number of messages in between sending the cancel command and receiving + notification that the cancel command is complete. + </doc> + + <rule name="post-cancel-transfer-resolution"> + <doc> + Canceling a subscription MUST NOT affect pending transfers. A transfer made prior to + canceling transfers to the destination MUST be able to be accepted, released, acquired, or + rejected after the subscription is canceled. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <field name="destination" type="destination" required="true"> + <exception name="subscription-not-found" error-code="not-found"> + <doc> + If the subscription specified by the destination is not found, the server MUST close the + session. + </doc> + </exception> + </field> + </command> + + <!-- - Command: message.set-flow-mode - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="set-flow-mode" code="0x9" label="set the flow control mode"> + <doc> + Sets the mode of flow control used for a given destination to either window or credit based + flow control. + + With credit based flow control, the sender of messages continually maintains its current + credit balance with the recipient. The credit balance consists of two values, a message + count, and a byte count. Whenever message data is sent, both counts must be decremented. + If either value reaches zero, the flow of message data must stop. Additional credit is + received via the message.flow command. + + The sender MUST NOT send partial assemblies. This means that if there is not enough byte + credit available to send a complete message, the sender must either wait or use message + fragmentation (see the fragment-properties header struct) to send the first part of the + message data in a complete assembly. + + Window based flow control is identical to credit based flow control, however message + transfer completion implicitly grants a single unit of message credit, and the size of the + message in byte credits for each completed message transfer. Completion of the transfer + command with session.completed is the only way credit is implicitly updated; message.accept, + message.release, message.reject, tx.commit and tx.rollback have no effect on the outstanding + credit balances. + </doc> + + <rule name="byte-accounting"> + <doc> + The byte count is decremented by the payload size of each transmitted frame with segment + type header or body appearing within a message.transfer command. Note that the payload + size is the frame size less the frame header size. + </doc> + </rule> + + <rule name="mode-switching"> + <doc> + Mode switching may only occur if both the byte and message credit balance are zero. There + are three ways for a recipient of messages to be sure that the sender's credit balances + are zero: + + 1) The recipient may send a message.stop command to the sender. When the recipient + receives notification of completion for the message.stop command, it knows that the + sender's credit is zero. + + 2) The recipient may perform the same steps described in (1) with the message.flush + command substituted for the message.stop command. + + 3) Immediately after a subscription is created with message.subscribe, the credit for + that destination is zero. + </doc> + </rule> + + <rule name="default-flow-mode"> + <doc> + Prior to receiving an explicit set-flow-mode command, a peer MUST consider the flow-mode + to be window. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="destination" type="destination"/> + <field name="flow-mode" type="flow-mode" required="true"> + <doc> + The new flow control mode. + </doc> + </field> + </command> + + <!-- - Command: message.flow - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="flow" code="0xa" label="control message flow"> + <doc> + This command controls the flow of message data to a given destination. It is used by the + recipient of messages to dynamically match the incoming rate of message flow to its + processing or forwarding capacity. Upon receipt of this command, the sender must add "value" + number of the specified unit to the available credit balance for the specified destination. + A value of (0xFFFFFFFF) indicates an infinite amount of credit. This disables any limit for + the given unit until the credit balance is zeroed with message.stop or message.flush. + </doc> + + <!-- throws no-such-destination --> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="destination" type="destination"/> + <field name="unit" type="credit-unit" required="true"> + <doc> + The unit of value. + </doc> + </field> + <field name="value" type="uint32"> + <doc> + If the value is not set then this indicates an infinite amount of credit. + </doc> + </field> + </command> + + <!-- - Command: message.flush - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="flush" code="0xb" label="force the sending of available messages"> + <doc> + Forces the sender to exhaust his credit supply. The sender's credit will always be zero when + this command completes. The command completes when immediately available message data has + been transferred, or when the credit supply is exhausted. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="destination" type="destination"/> + </command> + + <!-- - Command: message.stop - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="stop" code="0xc" label="stop the sending of messages"> + <doc> + On receipt of this command, a producer of messages MUST set his credit to zero for the given + destination. When notifying of completion, credit MUST be zero and no further messages will + be sent until such a time as further credit is received. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <field name="destination" type="destination"/> + </command> + + </class> + + <!-- == Class: tx ============================================================================ --> + + <class name="tx" code="0x5" label="work with standard transactions"> + <doc> + Standard transactions provide so-called "1.5 phase commit". We can ensure that work is never + lost, but there is a chance of confirmations being lost, so that messages may be resent. + Applications that use standard transactions must be able to detect and ignore duplicate + messages. + </doc> + + <doc type="grammar"> + tx = C:SELECT + / C:COMMIT + / C:ROLLBACK + </doc> + + <!-- XXX: this isn't really a rule, as stated there is no way for + a client library to implement this --> + <rule name="duplicate-tracking"> + <doc> + An client using standard transactions SHOULD be able to track all messages received within a + reasonable period, and thus detect and reject duplicates of the same message. It SHOULD NOT + pass these to the application layer. + </doc> + </rule> + + <role name="server" implement="SHOULD" /> + + <!-- - Command: tx.select - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="select" code="0x1" label="select standard transaction mode"> + <doc> + This command sets the session to use standard transactions. The client must use this command + exactly once on a session before using the Commit or Rollback commands. + </doc> + + <exception name="exactly-once" error-code="illegal-state"> + <doc> + A client MUST NOT select standard transactions on a session that is already transactional. + </doc> + </exception> + + <exception name="no-dtx" error-code="illegal-state"> + <doc> + A client MUST NOT select standard transactions on a session that is already enlisted in a + distributed transaction. + </doc> + </exception> + + <exception name="explicit-accepts" error-code="not-allowed"> + <doc> + On a session on which tx.select has been issued, a client MUST NOT issue a + message.subscribe command with the accept-mode property set to any value other than + explicit. Similarly a tx.select MUST NOT be issued on a session on which a there is a non + cancelled subscriber with accept-mode of none. + </doc> + </exception> + + <implement role="server" handle="MUST" /> + </command> + + <!-- - Command: tx.commit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="commit" code="0x2" label="commit the current transaction"> + <doc> + This command commits all messages published and accepted in the current transaction. A + new transaction starts immediately after a commit. + </doc> + <doc> + In more detail, the commit acts on all messages which have been transferred from the Client + to the Server, and on all acceptances of messages sent from Server to Client. Since the + commit acts on commands sent in the same direction as the commit command itself, there is no + ambiguity on the scope of the commands being committed. Further, the commit will not be + completed until all preceding commands which it affects have been completed. + </doc> + <doc> + Since transactions act on explicit accept commands, the only valid accept-mode for message + subscribers is explicit. For transferring messages from Client to Server (publishing) all + accept-modes are permitted. + </doc> + + <exception name="select-required" error-code="illegal-state"> + <doc> + A client MUST NOT issue tx.commit on a session that has not been selected for standard + transactions with tx.select. + </doc> + </exception> + + + + <implement role="server" handle="MUST" /> + </command> + + <!-- - Command: tx.rollback - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="rollback" code="0x3" label="abandon the current transaction"> + <doc> + This command abandons the current transaction. In particular the transfers from Client to + Server (publishes) and accepts of transfers from Server to Client which occurred in the + current transaction are discarded. A new transaction starts immediately after a rollback. + </doc> + <doc> + In more detail, when a rollback is issued, any the effects of transfers which occurred from + Client to Server are discarded. The Server will issue completion notification for all such + transfers prior to the completion of the rollback. Similarly the effects of any + message.accept issued from Client to Server prior to the issuance of the tx.rollback will be + discarded; and notification of completion for all such commands will be issued before the + issuance of the completion for the rollback. + </doc> + <doc> + After the completion of the rollback, the client will still hold the messages which it has + not yet accepted (including those for which accepts were previously issued within the + transaction); i.e. the messages remain "acquired". If the Client wishes to release those + messages back to the Server, then appropriate message.release commands must be issued. + </doc> + + <exception name="select-required" error-code="illegal-state"> + <doc> + A client MUST NOT issue tx.rollback on a session that has not been selected for standard + transactions with tx.select. + </doc> + </exception> + + <implement role="server" handle="MUST" /> + </command> + + </class> + + <!-- == Class: dtx =========================================================================== --> + + <class name="dtx" code="0x6" label="Demarcates dtx branches"> + <doc> + This provides the X-Open XA distributed transaction protocol support. It allows a session + to be selected for use with distributed transactions, the transactional boundaries for work on + that session to be demarcated and allows the transaction manager to coordinate transaction + outcomes. + </doc> + + <doc type="grammar"> + dtx-demarcation = C:SELECT *demarcation + demarcation = C:START C:END + </doc> + + <doc type="grammar"> + dtx-coordination = *coordination + coordination = command + / outcome + / recovery + command = C:SET-TIMEOUT + / C:GET-TIMEOUT + outcome = one-phase-commit + / one-phase-rollback + / two-phase-commit + / two-phase-rollback + one-phase-commit = C:COMMIT + one-phase-rollback = C:ROLLBACK + two-phase-commit = C:PREPARE C:COMMIT + two-phase-rollback = C:PREPARE C:ROLLBACK + recovery = C:RECOVER *recovery-outcome + recovery-outcome = one-phase-commit + / one-phase-rollback + / C:FORGET + + </doc> + + <rule name="transactionality"> + <doc> + Enabling XA transaction support on a session requires that the server MUST manage + transactions demarcated by start-end blocks. That is to say that on this XA-enabled session, + work undergone within transactional blocks is performed on behalf a transaction branch + whereas work performed outside of transactional blocks is NOT transactional. + </doc> + </rule> + + <role name="server" implement="MAY" /> + <role name="client" implement="MAY" /> + + <!-- XA domains --> + + <domain name="xa-status" type="uint16" label="XA return codes"> + <enum> + <choice name="xa-ok" value="0"> + <doc> + Normal execution completion (no error). + </doc> + </choice> + + <choice name="xa-rbrollback" value="1"> + <doc> + The rollback was caused for an unspecified reason. + </doc> + </choice> + + <choice name="xa-rbtimeout" value="2"> + <doc> + A transaction branch took too long. + </doc> + </choice> + + <choice name="xa-heurhaz" value="3"> + <doc> + The transaction branch may have been heuristically completed. + </doc> + </choice> + + <choice name="xa-heurcom" value="4"> + <doc> + The transaction branch has been heuristically committed. + </doc> + </choice> + + <choice name="xa-heurrb" value="5"> + <doc> + The transaction branch has been heuristically rolled back. + </doc> + </choice> + + <choice name="xa-heurmix" value="6"> + <doc> + The transaction branch has been heuristically committed and rolled back. + </doc> + </choice> + + <choice name="xa-rdonly" value="7"> + <doc> + The transaction branch was read-only and has been committed. + </doc> + </choice> + </enum> + </domain> + + <struct name="xa-result" size="4" code="0x1" pack="2"> + <field name="status" type="xa-status" required="true"/> + </struct> + + <!-- Struct for xid --> + + <struct name="xid" size="4" code="0x4" pack="2" label="dtx branch identifier"> + <doc> + An xid uniquely identifies a transaction branch. + </doc> + + <field name="format" type="uint32" label="implementation specific format code" + required="true"/> + <field name="global-id" type="vbin8" label="global transaction id" required="true"/> + <field name="branch-id" type="vbin8" label="branch qualifier" required="true"/> + </struct> + + <!-- - Command: dtx.select - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="select" code="0x1" label="Select dtx mode"> + <doc> + This command sets the session to use distributed transactions. The client must use this + command at least once on a session before using XA demarcation operations. + </doc> + + <implement role="server" handle="MAY" /> + </command> + + <!-- - Command: dtx.start - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="start" code="0x2" label="Start a dtx branch"> + <doc> + This command is called when messages should be produced and consumed on behalf a transaction + branch identified by xid. + </doc> + + <exception name="illegal-state" error-code="illegal-state"> + <doc> + If the command is invoked in an improper context (see class grammar) then the server MUST + send a session exception. + </doc> + </exception> + + <exception name="already-known" error-code="not-allowed"> + <doc> + If neither join nor resume is specified is specified and the transaction branch specified + by xid has previously been seen then the server MUST raise an exception. + </doc> + </exception> + + <exception name="join-and-resume" error-code="not-allowed"> + <doc> + If join and resume are specified then the server MUST raise an exception. + </doc> + </exception> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch to be started. + </doc> + + <exception name="unknown-xid" error-code="not-allowed"> + <doc> + If xid is already known by the broker then the server MUST raise an exception. + </doc> + </exception> + </field> + + <field name="join" type="bit" label="Join with existing xid flag"> + <doc> + Indicate whether this is joining an already associated xid. Indicate that the start + applies to joining a transaction previously seen. + </doc> + + <exception name="unsupported" error-code="not-implemented"> + <doc> + If the broker does not support join the server MUST raise an exception. + </doc> + </exception> + </field> + + <field name="resume" type="bit" label="Resume flag"> + <doc> + Indicate that the start applies to resuming a suspended transaction branch specified. + </doc> + </field> + + <result type="xa-result"> + <doc> + This confirms to the client that the transaction branch is started or specify the error + condition. + + The value of this field may be one of the following constants: + + xa-ok: Normal execution. + + xa-rbrollback: The broker marked the transaction branch rollback-only for an unspecified + reason. + + xa-rbtimeout: The work represented by this transaction branch took too long. + </doc> + </result> + </command> + + <!-- - Command: dtx.end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="end" code="0x3" label="End a dtx branch"> + <doc> + This command is called when the work done on behalf a transaction branch finishes or needs + to be suspended. + </doc> + + <exception name="illegal-state" error-code="illegal-state"> + <doc> + If the command is invoked in an improper context (see class grammar) then the server MUST + raise an exception. + </doc> + </exception> + + <exception name="suspend-and-fail" error-code="not-allowed"> + <doc> + If suspend and fail are specified then the server MUST raise an exception. + </doc> + </exception> + + <rule name="success"> + <doc> + If neither fail nor suspend are specified then the portion of work has completed + successfully. + </doc> + </rule> + + <rule name="session-closed"> + <doc> + When a session is closed then the currently associated transaction branches MUST be marked + rollback-only. + </doc> + </rule> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch to be ended. + </doc> + + <exception name="not-associated" error-code="illegal-state"> + <doc> + The session MUST be currently associated with the given xid (through an earlier start + call with the same xid). + </doc> + </exception> + </field> + + <field name="fail" type="bit" label="Failure flag"> + <doc> + If set, indicates that this portion of work has failed; otherwise this portion of work has + completed successfully. + </doc> + + <rule name="failure"> + <doc> + An implementation MAY elect to roll a transaction back if this failure notification is + received. Should an implementation elect to implement this behavior, and this bit is + set, then then the transaction branch SHOULD be marked as rollback-only and the end + result SHOULD have the xa-rbrollback status set. + </doc> + </rule> + </field> + + <field name="suspend" type="bit" label="Temporary suspension flag"> + <doc> + Indicates that the transaction branch is temporarily suspended in an incomplete state. + </doc> + + <rule name="resume"> + <doc> + The transaction context is in a suspended state and must be resumed via the start + command with resume specified. + </doc> + </rule> + + </field> + + <result type="xa-result"> + <doc> + This command confirms to the client that the transaction branch is ended or specify the + error condition. + + The value of this field may be one of the following constants: + + xa-ok: Normal execution. + + xa-rbrollback: The broker marked the transaction branch rollback-only for an unspecified + reason. If an implementation chooses to implement rollback-on-failure behavior, then + this value should be selected if the dtx.end.fail bit was set. + + xa-rbtimeout: The work represented by this transaction branch took too long. + </doc> + </result> + </command> + + <!-- - Command: dtx.commit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="commit" code="0x4" label="Commit work on dtx branch"> + <doc> + Commit the work done on behalf a transaction branch. This command commits the work + associated with xid. Any produced messages are made available and any consumed messages are + discarded. + </doc> + + <exception name="illegal-state" error-code="illegal-state"> + <doc> + If the command is invoked in an improper context (see class grammar) then the server MUST + raise an exception. + </doc> + </exception> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch to be committed. + </doc> + + <exception name="unknown-xid" error-code="not-found"> + <doc> + If xid is unknown (the transaction branch has not been started or has already been + ended) then the server MUST raise an exception. + </doc> + </exception> + + <exception name="not-disassociated" error-code="illegal-state"> + <doc> + If this command is called when xid is still associated with a session then the server + MUST raise an exception. + </doc> + </exception> + </field> + + <field name="one-phase" type="bit" label="One-phase optimization flag"> + <doc> + Used to indicate whether one-phase or two-phase commit is used. + </doc> + + <exception name="one-phase" error-code="illegal-state"> + <doc> + The one-phase bit MUST be set if a commit is sent without a preceding prepare. + </doc> + </exception> + + <exception name="two-phase" error-code="illegal-state"> + <doc> + The one-phase bit MUST NOT be set if the commit has been preceded by prepare. + </doc> + </exception> + </field> + + <result type="xa-result"> + <doc> + This confirms to the client that the transaction branch is committed or specify the + error condition. + + The value of this field may be one of the following constants: + + xa-ok: Normal execution + + xa-heurhaz: Due to some failure, the work done on behalf of the specified transaction + branch may have been heuristically completed. + + xa-heurcom: Due to a heuristic decision, the work done on behalf of the specified + transaction branch was committed. + + xa-heurrb: Due to a heuristic decision, the work done on behalf of the specified + transaction branch was rolled back. + + xa-heurmix: Due to a heuristic decision, the work done on behalf of the specified + transaction branch was partially committed and partially rolled back. + + xa-rbrollback: The broker marked the transaction branch rollback-only for an unspecified + reason. + + xa-rbtimeout: The work represented by this transaction branch took too long. + </doc> + </result> + </command> + + <!-- - Command: dtx.forget - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="forget" code="0x5" label="Discard dtx branch"> + <doc> + This command is called to forget about a heuristically completed transaction branch. + </doc> + + <exception name="illegal-state" error-code="illegal-state"> + <doc> + If the command is invoked in an improper context (see class grammar) then the server MUST + raise an exception. + </doc> + </exception> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch to be forgotten. + </doc> + + <exception name="unknown-xid" error-code="not-found"> + <doc> + If xid is unknown (the transaction branch has not been started or has already been + ended) then the server MUST raise an exception. + </doc> + </exception> + + <exception name="not-disassociated" error-code="illegal-state"> + <doc> + If this command is called when xid is still associated with a session then the server + MUST raise an exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: dtx.get-timeout - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="get-timeout" code="0x6" label="Obtain dtx timeout in seconds"> + <doc> + This command obtains the current transaction timeout value in seconds. If set-timeout was + not used prior to invoking this command, the return value is the default timeout; otherwise, + the value used in the previous set-timeout call is returned. + </doc> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch for getting the timeout. + </doc> + + <exception name="unknown-xid" error-code="not-found"> + <doc> + If xid is unknown (the transaction branch has not been started or has already been + ended) then the server MUST raise an exception. + </doc> + </exception> + </field> + + <result> + <struct name="get-timeout-result" size="4" code="0x2" pack="2"> + <doc> Returns the value of the timeout last specified through set-timeout. </doc> + + <field name="timeout" type="uint32" label="The current transaction timeout value" + required="true"> + <doc> The current transaction timeout value in seconds. </doc> + </field> + </struct> + </result> + </command> + + <!-- - Command: dtx.prepare - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="prepare" code="0x7" label="Prepare a dtx branch"> + <doc> + This command prepares for commitment any message produced or consumed on behalf of xid. + </doc> + + <exception name="illegal-state" error-code="illegal-state"> + <doc> + If the command is invoked in an improper context (see class grammar) then the server MUST + raise an exception. + </doc> + </exception> + + <rule name="obligation-1"> + <doc> + Once this command successfully returns it is guaranteed that the transaction branch may be + either committed or rolled back regardless of failures. + </doc> + </rule> + + <rule name="obligation-2"> + <doc> + The knowledge of xid cannot be erased before commit or rollback complete the branch. + </doc> + </rule> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch that can be prepared. + </doc> + + <exception name="unknown-xid" error-code="not-found"> + <doc> + If xid is unknown (the transaction branch has not been started or has already been + ended) then the server MUST raise an exception. + </doc> + </exception> + + <exception name="not-disassociated" error-code="illegal-state"> + <doc> + If this command is called when xid is still associated with a session then the server + MUST raise an exception. + </doc> + </exception> + </field> + + <result type="xa-result"> + <doc> + This command confirms to the client that the transaction branch is prepared or specify the + error condition. + + The value of this field may be one of the following constants: + + xa-ok: Normal execution. + + xa-rdonly: The transaction branch was read-only and has been committed. + + xa-rbrollback: The broker marked the transaction branch rollback-only for an unspecified + reason. + + xa-rbtimeout: The work represented by this transaction branch took too long. + </doc> + </result> + </command> + + <!-- - Command: dtx.recover - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="recover" code="0x8" label="Get prepared or completed xids"> + <doc> + This command is called to obtain a list of transaction branches that are in a prepared or + heuristically completed state. + </doc> + + <implement role="server" handle="MAY" /> + + <result> + <struct name="recover-result" size="4" code="0x3" pack="2"> + <doc> + Returns to the client a table with single item that is a sequence of transaction xids + that are in a prepared or heuristically completed state. + </doc> + + <field name="in-doubt" type="array" label="array of xids to be recovered" required="true"> + <doc> Array containing the xids to be recovered (xids that are in a prepared or + heuristically completed state). </doc> + + </field> + </struct> + </result> + </command> + + <!-- - Command: dtx.rollback - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="rollback" code="0x9" label="Rollback a dtx branch"> + <doc> + This command rolls back the work associated with xid. Any produced messages are discarded + and any consumed messages are re-enqueued. + </doc> + + <exception name="illegal-state" error-code="illegal-state"> + <doc> + If the command is invoked in an improper context (see class grammar) then the server MUST + raise an exception. + </doc> + </exception> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch that can be rolled back. + </doc> + + <exception name="unknown-xid" error-code="not-found"> + <doc> + If xid is unknown (the transaction branch has not been started or has already been + ended) then the server MUST raise an exception. + </doc> + </exception> + + <exception name="not-disassociated" error-code="illegal-state"> + <doc> + If this command is called when xid is still associated with a session then the server + MUST raise an exception. + </doc> + </exception> + </field> + + <result type="xa-result"> + <doc> + This command confirms to the client that the transaction branch is rolled back or specify + the error condition. + + The value of this field may be one of the following constants: + + xa-ok: Normal execution + + xa-heurhaz: Due to some failure, the work done on behalf of the specified transaction + branch may have been heuristically completed. + + xa-heurcom: Due to a heuristic decision, the work done on behalf of the specified + transaction branch was committed. + + xa-heurrb: Due to a heuristic decision, the work done on behalf of the specified + transaction branch was rolled back. + + xa-heurmix: Due to a heuristic decision, the work done on behalf of the specified + transaction branch was partially committed and partially rolled back. + + xa-rbrollback: The broker marked the transaction branch rollback-only for an unspecified + reason. + + xa-rbtimeout: The work represented by this transaction branch took too long. + </doc> + </result> + </command> + + <!-- - Command: dtx.set-timeout - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="set-timeout" code="0xa" label="Set dtx timeout value"> + <doc> + Sets the specified transaction branch timeout value in seconds. + </doc> + + <rule name="effective"> + <doc> + Once set, this timeout value is effective until this command is reinvoked with a different + value. + </doc> + </rule> + + <rule name="reset"> + <doc> + A value of zero resets the timeout value to the default value. + </doc> + </rule> + + <implement role="server" handle="MAY" /> + + <field name="xid" type="xid" label="Transaction xid" required="true"> + <doc> + Specifies the xid of the transaction branch for setting the timeout. + </doc> + + <exception name="unknown-xid" error-code="not-found"> + <doc> + If xid is unknown (the transaction branch has not been started or has already been + ended) then the server MUST raise an exception. + </doc> + </exception> + + </field> + + <field name="timeout" type="uint32" label="Dtx timeout in seconds" required="true"> + <doc> + The transaction timeout value in seconds. + </doc> + </field> + </command> + + </class> + + <!-- == Class: exchange ====================================================================== --> + + <class name="exchange" code="0x7" label="work with exchanges"> + <doc> + Exchanges match and distribute messages across queues. Exchanges can be configured in the + server or created at runtime. + </doc> + + <doc type="grammar"> + exchange = C:DECLARE + / C:DELETE + / C:QUERY + </doc> + + <rule name="required-types"> + <doc> + The server MUST implement these standard exchange types: fanout, direct. + </doc> + <doc type="scenario"> + Client attempts to declare an exchange with each of these standard types. + </doc> + </rule> + + <rule name="recommended-types"> + <doc> + The server SHOULD implement these standard exchange types: topic, headers. + </doc> + <doc type="scenario"> + Client attempts to declare an exchange with each of these standard types. + </doc> + </rule> + + <rule name="required-instances"> + <doc> + The server MUST, in each virtual host, pre-declare an exchange instance for each standard + exchange type that it implements, where the name of the exchange instance, if defined, is + "amq." followed by the exchange type name. + + The server MUST, in each virtual host, pre-declare at least two direct exchange instances: + one named "amq.direct", the other with no public name that serves as a default exchange for + publish commands (such as message.transfer). + </doc> + <doc type="scenario"> + Client creates a temporary queue and attempts to bind to each required exchange instance + ("amq.fanout", "amq.direct", "amq.topic", and "amq.headers" if those types are defined). + </doc> + </rule> + + <rule name="default-exchange"> + <doc> + The server MUST pre-declare a direct exchange with no public name to act as the default + exchange for content publish commands (such as message.transfer) and for default queue + bindings. + </doc> + <doc type="scenario"> + Client checks that the default exchange is active by publishing a message with a suitable + routing key but without specifying the exchange name, then ensuring that the message arrives + in the queue correctly. + </doc> + </rule> + + <rule name="default-access"> + <doc> + The default exchange MUST NOT be accessible to the client except by specifying an empty + exchange name in a content publish command (such as message.transfer). That is, the server + must not let clients explicitly bind, unbind, delete, or make any other reference to this + exchange. + </doc> + </rule> + + <rule name="extensions"> + <doc> + The server MAY implement other exchange types as wanted. + </doc> + </rule> + + <role name="server" implement="MUST" /> + <role name="client" implement="MUST" /> + + <domain name="name" type="str8" label="exchange name"> + <doc> + The exchange name is a client-selected string that identifies the exchange for publish + commands. Exchange names may consist of any mixture of digits, letters, and underscores. + Exchange names are scoped by the virtual host. + </doc> + </domain> + + <!-- - Command: exchange.declare - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="declare" code="0x1" label="verify exchange exists, create if needed"> + <doc> + This command creates an exchange if it does not already exist, and if the exchange exists, + verifies that it is of the correct and expected class. + </doc> + + <rule name="minimum"> + <doc> + The server SHOULD support a minimum of 16 exchanges per virtual host and ideally, impose + no limit except as defined by available resources. + </doc> + <doc type="scenario"> + The client creates as many exchanges as it can until the server reports an error; the + number of exchanges successfully created must be at least sixteen. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <field name="exchange" type="name" required="true"> + <exception name="reserved-names" error-code="not-allowed"> + <doc> + Exchange names starting with "amq." are reserved for pre-declared and standardized + exchanges. The client MUST NOT attempt to create an exchange starting with "amq.". + </doc> + </exception> + + <exception name="exchange-name-required" error-code="invalid-argument"> + <doc> + The name of the exchange MUST NOT be a blank or empty string. + </doc> + </exception> + </field> + + <field name="type" type="str8" label="exchange type" required="true"> + <doc> + Each exchange belongs to one of a set of exchange types implemented by the server. The + exchange types define the functionality of the exchange - i.e. how messages are routed + through it. It is not valid or meaningful to attempt to change the type of an existing + exchange. + </doc> + + <exception name="typed" error-code="not-allowed"> + <doc> + Exchanges cannot be redeclared with different types. The client MUST NOT attempt to + redeclare an existing exchange with a different type than used in the original + exchange.declare command. + </doc> + </exception> + + <exception name="exchange-type-not-found" error-code="not-found"> + <doc> + If the client attempts to create an exchange which the server does not recognize, an + exception MUST be sent. + </doc> + </exception> + </field> + + <field name="alternate-exchange" type="name" label= "exchange name for unroutable messages"> + <doc> + In the event that a message cannot be routed, this is the name of the exchange to which + the message will be sent. Messages transferred using message.transfer will be routed to + the alternate-exchange only if they are sent with the "none" accept-mode, and the + discard-unroutable delivery property is set to false, and there is no queue to route to + for the given message according to the bindings on this exchange. + </doc> + + <rule name="empty-name"> + <doc> + If alternate-exchange is not set (its name is an empty string), unroutable messages + that would be sent to the alternate-exchange MUST be dropped silently. + </doc> + </rule> + + <exception name="pre-existing-exchange" error-code="not-allowed"> + <doc> + If the alternate-exchange is not empty and if the exchange already exists with a + different alternate-exchange, then the declaration MUST result in an exception. + </doc> + </exception> + + <rule name="double-failure"> + <doc> + A message which is being routed to a alternate exchange, MUST NOT be re-routed to a + secondary alternate exchange if it fails to route in the primary alternate exchange. + After such a failure, the message MUST be dropped. This prevents looping. + </doc> + </rule> + </field> + + <field name="passive" type="bit" label="do not create exchange"> + <doc> + If set, the server will not create the exchange. The client can use this to check whether + an exchange exists without modifying the server state. + </doc> + <exception name="not-found" error-code="not-found"> + <doc> + If set, and the exchange does not already exist, the server MUST raise an exception. + </doc> + </exception> + </field> + + <field name="durable" type="bit" label="request a durable exchange"> + <doc> + If set when creating a new exchange, the exchange will be marked as durable. Durable + exchanges remain active when a server restarts. Non-durable exchanges (transient + exchanges) are purged if/when a server restarts. + </doc> + + <rule name="support"> + <doc> + The server MUST support both durable and transient exchanges. + </doc> + </rule> + + <rule name="sticky"> + <doc> + The server MUST ignore the durable field if the exchange already exists. + </doc> + </rule> + </field> + + <field name="auto-delete" type="bit" label="auto-delete when unused"> + <doc> + If set, the exchange is deleted automatically when there remain no bindings between the + exchange and any queue. Such an exchange will not be automatically deleted until at least + one binding has been made to prevent the immediate deletion of the exchange upon creation. + </doc> + <rule name="sticky"> + <doc> + The server MUST ignore the auto-delete field if the exchange already exists. + </doc> + </rule> + </field> + + <field name="arguments" type="map" label="arguments for declaration"> + <doc> + A set of arguments for the declaration. The syntax and semantics of these arguments + depends on the server implementation. This field is ignored if passive is 1. + </doc> + + <exception name="unknown-argument" error-code="not-implemented"> + <doc> + If the arguments field contains arguments which are not understood by the server, + it MUST raise an exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: exchange.delete - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="delete" code="0x2" label="delete an exchange"> + <doc> + This command deletes an exchange. When an exchange is deleted all queue bindings on the + exchange are cancelled. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="exchange" type="name" required="true"> + <exception name="exists" error-code="not-found"> + <doc> + The client MUST NOT attempt to delete an exchange that does not exist. + </doc> + </exception> + + <exception name="exchange-name-required" error-code="invalid-argument"> + <doc> + The name of the exchange MUST NOT be a missing or empty string. + </doc> + </exception> + + <exception name="used-as-alternate" error-code="not-allowed"> + <doc> + An exchange MUST NOT be deleted if it is in use as an alternate-exchange by a queue or + by another exchange. + </doc> + </exception> + + </field> + + <field name="if-unused" type="bit" label="delete only if unused"> + <doc> + If set, the server will only delete the exchange if it has no queue bindings. If the + exchange has queue bindings the server does not delete it but raises an exception + instead. + </doc> + <exception name="exchange-in-use" error-code="precondition-failed"> + <doc> + If the exchange has queue bindings, and the if-unused flag is set, the server MUST NOT + delete the exchange, but MUST raise and exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: exchange.query - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="query" code="0x3" label="request information about an exchange"> + <doc> + This command is used to request information on a particular exchange. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="name" type="str8" label="the exchange name"> + <doc> + The name of the exchange for which information is requested. If not specified explicitly + the default exchange is implied. + </doc> + </field> + + <result> + <struct name="exchange-query-result" size="4" code="0x1" pack="2"> + <doc> + This is sent in response to a query request and conveys information on a particular + exchange. + </doc> + + <field name="type" type="str8" label="indicate the exchange type"> + <doc> + The type of the exchange. Will be empty if the exchange is not found. + </doc> + </field> + + <field name="durable" type="bit" label="indicate the durability"> + <doc> + The durability of the exchange, i.e. if set the exchange is durable. Will not be set + if the exchange is not found. + </doc> + </field> + + <field name="not-found" type="bit" label="indicate an unknown exchange"> + <doc> + If set, the exchange for which information was requested is not known. + </doc> + </field> + + <field name="arguments" type="map" label="other unspecified exchange properties"> + <doc> + A set of properties of the exchange whose syntax and semantics depends on the server + implementation. Will be empty if the exchange is not found. + </doc> + </field> + </struct> + </result> + </command> + + <!-- - Command: exchange.bind - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="bind" code="0x4" label="bind queue to an exchange"> + <doc> This command binds a queue to an exchange. Until a queue is bound it will not receive + any messages. In a classic messaging model, store-and-forward queues are bound to a direct + exchange and subscription queues are bound to a topic exchange. </doc> + + <rule name="duplicates"> + <doc> + A server MUST ignore duplicate bindings - that is, two or more bind commands with the + same exchange, queue, and binding-key - without treating these as an error. The value of + the arguments used for the binding MUST NOT be altered by subsequent binding requests. + </doc> + <doc type="scenario"> + A client binds a named queue to an exchange. The client then repeats the bind (with + identical exchange, queue, and binding-key). The second binding should use a different + value for the arguments field. + </doc> + </rule> + + <rule name="durable-exchange"> + <doc> Bindings between durable queues and durable exchanges are automatically durable and + the server MUST restore such bindings after a server restart. </doc> + <doc type="scenario"> A server creates a named durable queue and binds it to a durable + exchange. The server is restarted. The client then attempts to use the queue/exchange + combination. </doc> + </rule> + + <rule name="binding-count"> + <doc> The server SHOULD support at least 4 bindings per queue, and ideally, impose no limit + except as defined by available resources. </doc> + <doc type="scenario"> A client creates a named queue and attempts to bind it to 4 different + exchanges. </doc> + </rule> + + <rule name="multiple-bindings"> + <doc> Where more than one binding exists between a particular exchange instance and a + particular queue instance any given message published to that exchange should be delivered + to that queue at most once, regardless of how many distinct bindings match. </doc> + <doc type="scenario"> A client creates a named queue and binds it to the same topic exchange + at least three times using intersecting binding-keys (for example, "animals.*", + "animals.dogs.*", "animal.dogs.chihuahua"). Verify that a message matching all the + bindings (using previous example, routing key = "animal.dogs.chihuahua") is delivered once + only. </doc> + </rule> + + <implement role="server" handle="MUST"/> + + <field name="queue" type="queue.name" required="true"> + <doc> Specifies the name of the queue to bind. </doc> + + <exception name="empty-queue" error-code="invalid-argument"> + <doc> A client MUST NOT be allowed to bind a non-existent and unnamed queue (i.e. empty + queue name) to an exchange. </doc> + <doc type="scenario"> A client attempts to bind with an unnamed (empty) queue name to an + exchange. </doc> + </exception> + + <exception name="queue-existence" error-code="not-found"> + <doc> A client MUST NOT be allowed to bind a non-existent queue (i.e. not previously + declared) to an exchange. </doc> + <doc type="scenario"> A client attempts to bind an undeclared queue name to an exchange. + </doc> + </exception> + </field> + + <field name="exchange" type="name" label="name of the exchange to bind to" required="true"> + <exception name="exchange-existence" error-code="not-found"> + <doc> A client MUST NOT be allowed to bind a queue to a non-existent exchange. </doc> + <doc type="scenario"> A client attempts to bind a named queue to a undeclared exchange. + </doc> + </exception> + + <exception name="exchange-name-required" error-code="invalid-argument"> + <doc> The name of the exchange MUST NOT be a blank or empty string. </doc> + </exception> + </field> + + <field name="binding-key" type="str8" + label="identifies a binding between a given exchange and queue" required="true"> + <doc> The binding-key uniquely identifies a binding between a given (exchange, queue) pair. + Depending on the exchange configuration, the binding key may be matched against the + message routing key in order to make routing decisions. The match algorithm depends on the + exchange type. Some exchange types may ignore the binding key when making routing + decisions. Refer to the specific exchange type documentation. The meaning of an empty + binding key depends on the exchange implementation. </doc> + </field> + + <field name="arguments" type="map" label="arguments for binding"> + <doc> A set of arguments for the binding. The syntax and semantics of these arguments + depends on the exchange class. </doc> + + <exception name="unknown-argument" error-code="not-implemented"> + <doc> If the arguments field contains arguments which are not understood by the server, it + MUST raise an exception. </doc> + </exception> + </field> + </command> + + <!-- - Command: exchange.unbind - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="unbind" code="0x5" label="unbind a queue from an exchange"> + <doc> + This command unbinds a queue from an exchange. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="queue" type="queue.name" required="true"> + <doc> + Specifies the name of the queue to unbind. + </doc> + <exception name="non-existent-queue" error-code="not-found"> + <doc> + If the queue does not exist the server MUST raise an exception. + </doc> + </exception> + </field> + + <field name="exchange" type="name" required="true"> + <doc> + The name of the exchange to unbind from. + </doc> + + <exception name="non-existent-exchange" error-code="not-found"> + <doc> + If the exchange does not exist the server MUST raise an exception. + </doc> + </exception> + + <exception name="exchange-name-required" error-code="invalid-argument"> + <doc> + The name of the exchange MUST NOT be a blank or empty string. + </doc> + </exception> + </field> + + <field name="binding-key" type="str8" label="the key of the binding" required="true"> + <doc> + Specifies the binding-key of the binding to unbind. + </doc> + + <exception name="non-existent-binding-key" error-code="not-found"> + <doc> + If there is no matching binding-key the server MUST raise an exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: exchange.bound - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="bound" code="0x6" label="request information about bindings to an exchange"> + <doc> + This command is used to request information on the bindings to a particular exchange. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="exchange" type="str8" label="the exchange name"> + <doc> + The name of the exchange for which binding information is being requested. If not + specified explicitly the default exchange is implied. + </doc> + </field> + + <field name="queue" type="str8" label="a queue name" required="true"> + <doc> + If populated then determine whether the given queue is bound to the exchange. + </doc> + </field> + + <field name="binding-key" type="str8" label="a binding-key"> + <doc> + If populated defines the binding-key of the binding of interest, if not populated the + request will ignore the binding-key on bindings when searching for a match. + </doc> + </field> + + <field name="arguments" type="map" label="a set of binding arguments"> + <doc> + If populated defines the arguments of the binding of interest if not populated the request + will ignore the arguments on bindings when searching for a match + </doc> + </field> + + <result> + <struct name="exchange-bound-result" size="4" code="0x2" pack="2"> + <field name="exchange-not-found" type="bit" label="indicate an unknown exchange"> + <doc> + If set, the exchange for which information was requested is not known. + </doc> + </field> + + <field name="queue-not-found" type="bit" label="indicate an unknown queue"> + <doc> + If set, the queue specified is not known. + </doc> + </field> + + <field name="queue-not-matched" type="bit" label="indicate no matching queue"> + <doc> + A bit which if set indicates that no binding was found from the specified exchange to + the specified queue. + </doc> + </field> + + <field name="key-not-matched" type="bit" label="indicate no matching binding-key"> + <doc> + A bit which if set indicates that no binding was found from the specified exchange + with the specified binding-key. + </doc> + </field> + + <field name="args-not-matched" type="bit" label="indicate no matching arguments"> + <doc> + A bit which if set indicates that no binding was found from the specified exchange + with the specified arguments. + </doc> + </field> + </struct> + </result> + </command> + + </class> + + <!-- == Class: queue ========================================================================= --> + + <class name="queue" code="0x8" label="work with queues"> + <doc> + Queues store and forward messages. Queues can be configured in the server or created at + runtime. Queues must be attached to at least one exchange in order to receive messages from + publishers. + </doc> + + <doc type="grammar"> + queue = C:DECLARE + / C:BIND + / C:PURGE + / C:DELETE + / C:QUERY + / C:UNBIND + </doc> + + <rule name="any-content"> + <doc> + A server MUST allow any content class to be sent to any queue, in any mix, and queue and + deliver these content classes independently. Note that all commands that fetch content off + queues are specific to a given content class. + </doc> + <doc type="scenario"> + Client creates an exchange of each standard type and several queues that it binds to each + exchange. It must then successfully send each of the standard content types to each of the + available queues. + </doc> + </rule> + + <role name="server" implement="MUST" /> + <role name="client" implement="MUST" /> + + <domain name="name" type="str8" label="queue name"> + <doc> + The queue name identifies the queue within the virtual host. Queue names must have a length + of between 1 and 255 characters inclusive, must start with a digit, letter or underscores + ('_') character, and must be otherwise encoded in UTF-8. + </doc> + </domain> + + <!-- - Command: queue.declare - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="declare" code="0x1" label="declare queue"> + <doc> + This command creates or checks a queue. When creating a new queue the client can specify + various properties that control the durability of the queue and its contents, and the level + of sharing for the queue. + </doc> + + <rule name="default-binding"> + <doc> + The server MUST create a default binding for a newly-created queue to the default + exchange, which is an exchange of type 'direct' and use the queue name as the binding-key. + </doc> + <doc type="scenario"> + Client creates a new queue, and then without explicitly binding it to an exchange, + attempts to send a message through the default exchange binding, i.e. publish a message to + the empty exchange, with the queue name as binding-key. + </doc> + </rule> + + <rule name="minimum-queues"> + <doc> + The server SHOULD support a minimum of 256 queues per virtual host and ideally, impose no + limit except as defined by available resources. + </doc> + <doc type="scenario"> + Client attempts to create as many queues as it can until the server reports an error. The + resulting count must at least be 256. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <field name="queue" type="name" required="true"> + <exception name="reserved-prefix" error-code="not-allowed"> + <doc> + Queue names starting with "amq." are reserved for pre-declared and standardized server + queues. A client MUST NOT attempt to declare a queue with a name that starts with "amq." + and the passive option set to zero. + </doc> + <doc type="scenario"> + A client attempts to create a queue with a name starting with "amq." and with the + passive option set to zero. + </doc> + </exception> + </field> + + <field name="alternate-exchange" type="exchange.name" + label= "exchange name for messages with exceptions"> + <doc> + The alternate-exchange field specifies how messages on this queue should be treated when + they are rejected by a subscriber, or when they are orphaned by queue deletion. When + present, rejected or orphaned messages MUST be routed to the alternate-exchange. In all + cases the messages MUST be removed from the queue. + </doc> + + <exception name="pre-existing-exchange" error-code="not-allowed"> + <doc> + If the alternate-exchange is not empty and if the queue already exists with a different + alternate-exchange, then the declaration MUST result in an exception. + </doc> + </exception> + + <exception name="unknown-exchange" error-code="not-found"> + <doc> + if the alternate-exchange does not match the name of any existing exchange on the + server, then an exception must be raised. + </doc> + </exception> + </field> + + <field name="passive" type="bit" label="do not create queue"> + <doc> + If set, the server will not create the queue. This field allows the client to assert the + presence of a queue without modifying the server state. + </doc> + + <exception name="passive" error-code="not-found"> + <doc> + The client MAY ask the server to assert that a queue exists without creating the queue + if not. If the queue does not exist, the server treats this as a failure. + </doc> + <doc type="scenario"> + Client declares an existing queue with the passive option and expects the command to + succeed. Client then attempts to declare a non-existent queue with the passive option, + and the server must close the session with the correct exception. + </doc> + </exception> + </field> + + <field name="durable" type="bit" label="request a durable queue"> + <doc> + If set when creating a new queue, the queue will be marked as durable. Durable queues + remain active when a server restarts. Non-durable queues (transient queues) are purged + if/when a server restarts. Note that durable queues do not necessarily hold persistent + messages, although it does not make sense to send persistent messages to a transient + queue. + </doc> + + <rule name="persistence"> + <doc> + The queue definition MUST survive the server losing all transient memory, e.g. a + machine restart. + </doc> + <doc type="scenario"> + Client creates a durable queue; server is then restarted. Client then attempts to send + message to the queue. The message should be successfully delivered. + </doc> + </rule> + + <rule name="types"> + <doc> + The server MUST support both durable and transient queues. + </doc> + <doc type="scenario"> + A client creates two named queues, one durable and one transient. + </doc> + </rule> + + <rule name="pre-existence"> + <doc> + The server MUST ignore the durable field if the queue already exists. + </doc> + <doc type="scenario"> + A client creates two named queues, one durable and one transient. The client then + attempts to declare the two queues using the same names again, but reversing the value + of the durable flag in each case. Verify that the queues still exist with the original + durable flag values. + </doc> + </rule> + </field> + + <field name="exclusive" type="bit" label="request an exclusive queue"> + <doc> + Exclusive queues can only be used from one session at a time. Once a session + declares an exclusive queue, that queue cannot be used by any other session until the + declaring session closes. + </doc> + + <rule name="types"> + <doc> + The server MUST support both exclusive (private) and non-exclusive (shared) queues. + </doc> + <doc type="scenario"> + A client creates two named queues, one exclusive and one non-exclusive. + </doc> + </rule> + + <exception name="in-use" error-code="resource-locked"> + <doc> + If the server receives a declare, bind, consume or get request for a queue that has been + declared as exclusive by an existing client session, it MUST raise an exception. + </doc> + <doc type="scenario"> + A client declares an exclusive named queue. A second client on a different session + attempts to declare a queue of the same name. + </doc> + </exception> + </field> + + <field name="auto-delete" type="bit" label="auto-delete queue when unused"> + <doc> + If this field is set and the exclusive field is also set, then the queue MUST be deleted + when the session closes. + + If this field is set and the exclusive field is not set the queue is deleted when all + the consumers have finished using it. Last consumer can be cancelled either explicitly + or because its session is closed. If there was no consumer ever on the queue, it won't + be deleted. + </doc> + + <rule name="pre-existence"> + <doc> + The server MUST ignore the auto-delete field if the queue already exists. + </doc> + <doc type="scenario"> + A client creates two named queues, one as auto-delete and one explicit-delete. The + client then attempts to declare the two queues using the same names again, but reversing + the value of the auto-delete field in each case. Verify that the queues still exist with + the original auto-delete flag values. + </doc> + </rule> + </field> + + <field name="arguments" type="map" label="arguments for declaration"> + <doc> + A set of arguments for the declaration. The syntax and semantics of these arguments + depends on the server implementation. This field is ignored if passive is 1. + </doc> + + <exception name="unknown-argument" error-code="not-implemented"> + <doc> + If the arguments field contains arguments which are not understood by the server, + it MUST raise an exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: queue.delete - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="delete" code="0x2" label="delete a queue"> + <doc> + This command deletes a queue. When a queue is deleted any pending messages are sent to the + alternate-exchange if defined, or discarded if it is not. + </doc> + + + <implement role="server" handle="MUST" /> + + <field name="queue" type="name" required="true"> + <doc> + Specifies the name of the queue to delete. + </doc> + + <exception name="empty-name" error-code="invalid-argument"> + <doc> + If the queue name in this command is empty, the server MUST raise an exception. + </doc> + </exception> + + <exception name="queue-exists" error-code="not-found"> + <doc> + The queue must exist. If the client attempts to delete a non-existing queue the server + MUST raise an exception. + </doc> + </exception> + </field> + + <field name="if-unused" type="bit" label="delete only if unused"> + <doc> + If set, the server will only delete the queue if it has no consumers. If the queue has + consumers the server does does not delete it but raises an exception instead. + </doc> + + <exception name="if-unused-flag" error-code="precondition-failed"> + <doc> + The server MUST respect the if-unused flag when deleting a queue. + </doc> + </exception> + </field> + + <field name="if-empty" type="bit" label="delete only if empty"> + <doc> + If set, the server will only delete the queue if it has no messages. + </doc> + <exception name="not-empty" error-code="precondition-failed"> + <doc> + If the queue is not empty the server MUST raise an exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: queue.purge - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="purge" code="0x3" label="purge a queue"> + <doc> + This command removes all messages from a queue. It does not cancel subscribers. Purged + messages are deleted without any formal "undo" mechanism. + </doc> + + <rule name="empty"> + <doc> + A call to purge MUST result in an empty queue. + </doc> + </rule> + + <rule name="pending-messages"> + <doc> + The server MUST NOT purge messages that have already been sent to a client but not yet + accepted. + </doc> + </rule> + + <rule name="purge-recovery"> + <doc> + The server MAY implement a purge queue or log that allows system administrators to recover + accidentally-purged messages. The server SHOULD NOT keep purged messages in the same + storage spaces as the live messages since the volumes of purged messages may get very + large. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <field name="queue" type="name" required="true"> + <doc> + Specifies the name of the queue to purge. + </doc> + + <exception name="empty-name" error-code="invalid-argument"> + <doc> + If the the queue name in this command is empty, the server MUST raise an exception. + </doc> + </exception> + + <exception name="queue-exists" error-code="not-found"> + <doc> + The queue MUST exist. Attempting to purge a non-existing queue MUST cause an exception. + </doc> + </exception> + </field> + </command> + + <!-- - Command: queue.query - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="query" code="0x4" label="request information about a queue"> + <doc> + This command requests information about a queue. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="queue" type="name" label="the queried queue" required="true"/> + + <result> + <struct name="queue-query-result" size="4" code="0x1" pack="2"> + <doc> + This is sent in response to queue.query, and conveys the requested information about a + queue. If no queue with the specified name exists then none of the fields within the + returned result struct will be populated. + </doc> + + <field name="queue" type="name" required="true"> + <doc> + Reports the name of the queue. + </doc> + </field> + + <field name="alternate-exchange" type="exchange.name" /> + + <field name="durable" type="bit" /> + + <field name="exclusive" type="bit" /> + + <field name="auto-delete" type="bit" /> + + <field name="arguments" type="map" /> + + <field name="message-count" type="uint32" label="number of messages in queue" + required="true"> + <doc> Reports the number of messages in the queue. </doc> + </field> + + <field name="subscriber-count" type="uint32" label="number of subscribers" + required="true"> + <doc> + Reports the number of subscribers for the queue. + </doc> + </field> + </struct> + </result> + </command> + + </class> + + <!-- == Class: file ========================================================================== --> + + <class name="file" code="0x9" label="work with file content"> + <doc> + The file class provides commands that support reliable file transfer. File messages have a + specific set of properties that are required for interoperability with file transfer + applications. File messages and acknowledgements are subject to session transactions. Note + that the file class does not provide message browsing commands; these are not compatible with + the staging model. Applications that need browsable file transfer should use Message content + and the Message class. + </doc> + + <doc type="grammar"> + file = C:QOS S:QOS-OK + / C:CONSUME S:CONSUME-OK + / C:CANCEL + / C:OPEN S:OPEN-OK C:STAGE content + / S:OPEN C:OPEN-OK S:STAGE content + / C:PUBLISH + / S:DELIVER + / S:RETURN + / C:ACK + / C:REJECT + </doc> + + <rule name="reliable-storage"> + <doc> + The server MUST make a best-effort to hold file messages on a reliable storage mechanism. + </doc> + </rule> + + <rule name="no-discard"> + <doc> + The server MUST NOT discard a file message in case of a queue overflow. The server MUST use + the Session.Flow command to slow or stop a file message publisher when necessary. + </doc> + </rule> + + <rule name="priority-levels"> + <doc> + The server MUST implement at least 2 priority levels for file messages, where priorities 0-4 + and 5-9 are treated as two distinct levels. The server MAY implement up to 10 priority + levels. + </doc> + </rule> + + <rule name="acknowledgement-support"> + <doc> + The server MUST support both automatic and explicit acknowledgements on file content. + </doc> + </rule> + + <role name="server" implement="MAY" /> + <role name="client" implement="MAY" /> + + <!-- These are the properties for a File content --> + <struct name="file-properties" size="4" code="0x1" pack="2"> + <field name="content-type" type="str8" label="MIME content type" /> + <field name="content-encoding" type="str8" label="MIME content encoding" /> + <field name="headers" type="map" label="message header field table" /> + <field name="priority" type="uint8" label="message priority, 0 to 9" /> + <field name="reply-to" type="str8" label="destination to reply to" /> + <field name="message-id" type="str8" label="application message identifier" /> + <field name="filename" type="str8" label="message filename" /> + <field name="timestamp" type="datetime" label="message timestamp" /> + <!-- This field is deprecated pending review --> + <field name="cluster-id" type="str8" label="intra-cluster routing identifier" /> + </struct> + + <domain name="return-code" type="uint16" label="return code from server"> + <doc> + The return code. The AMQP return codes are defined by this enum. + </doc> + <enum> + <choice name="content-too-large" value="311"> + <doc> + The client attempted to transfer content larger than the server could accept. + </doc> + </choice> + + <choice name="no-route" value="312"> + <doc> + The exchange cannot route a message, most likely due to an invalid routing key. Only + when the mandatory flag is set. + </doc> + </choice> + + <choice name="no-consumers" value="313"> + <doc> + The exchange cannot deliver to a consumer when the immediate flag is set. As a result of + pending data on the queue or the absence of any consumers of the queue. + </doc> + </choice> + </enum> + </domain> + + <!-- - Command: file.qos - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="qos" code="0x1" label="specify quality of service"> + <doc> + This command requests a specific quality of service. The QoS can be specified for the + current session or for all sessions on the connection. The particular properties and + semantics of a qos command always depend on the content class semantics. Though the qos + command could in principle apply to both peers, it is currently meaningful only for the + server. + </doc> + + <implement role="server" handle="MUST" /> + + <response name="qos-ok" /> + + <field name="prefetch-size" type="uint32" label="pre-fetch window in octets"> + <doc> + The client can request that messages be sent in advance so that when the client finishes + processing a message, the following message is already held locally, rather than needing + to be sent within the session. Pre-fetching gives a performance improvement. This field + specifies the pre-fetch window size in octets. May be set to zero, meaning "no specific + limit". Note that other pre-fetch limits may still apply. The prefetch-size is ignored if + the no-ack option is set. + </doc> + </field> + + <field name="prefetch-count" type="uint16" label="pre-fetch window in messages"> + <doc> + Specifies a pre-fetch window in terms of whole messages. This is compatible with some file + API implementations. This field may be used in combination with the prefetch-size field; a + message will only be sent in advance if both pre-fetch windows (and those at the session + and connection level) allow it. The prefetch-count is ignored if the no-ack option is set. + </doc> + + <rule name="prefetch-discretion"> + <doc> + The server MAY send less data in advance than allowed by the client's specified + pre-fetch windows but it MUST NOT send more. + </doc> + </rule> + </field> + + <field name="global" type="bit" label="apply to entire connection"> + <doc> + By default the QoS settings apply to the current session only. If this field is set, they + are applied to the entire connection. + </doc> + </field> + </command> + + <!-- - Command: file.qos-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="qos-ok" code="0x2" label="confirm the requested qos"> + <doc> + This command tells the client that the requested QoS levels could be handled by the server. + The requested QoS applies to all active consumers until a new QoS is defined. + </doc> + + <implement role="client" handle="MUST" /> + </command> + + <!-- - Command: file.consume - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="consume" code="0x3" label="start a queue consumer"> + <doc> + This command asks the server to start a "consumer", which is a transient request for + messages from a specific queue. Consumers last as long as the session they were created on, + or until the client cancels them. + </doc> + + <rule name="min-consumers"> + <doc> + The server SHOULD support at least 16 consumers per queue, unless the queue was declared + as private, and ideally, impose no limit except as defined by available resources. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <response name="consume-ok" /> + + <field name="queue" type="queue.name"> + <doc> + Specifies the name of the queue to consume from. + </doc> + + <exception name="queue-exists-if-empty" error-code="not-allowed"> + <doc> + If the queue name in this command is empty, the server MUST raise an exception. + </doc> + </exception> + </field> + + <field name="consumer-tag" type="str8"> + <doc> + Specifies the identifier for the consumer. The consumer tag is local to a connection, so + two clients can use the same consumer tags. + </doc> + + <exception name="not-existing-consumer" error-code="not-allowed"> + <doc> + The tag MUST NOT refer to an existing consumer. If the client attempts to create two + consumers with the same non-empty tag the server MUST raise an exception. + </doc> + </exception> + + <exception name="not-empty-consumer-tag" error-code="not-allowed"> + <doc> + The client MUST NOT specify a tag that is empty or blank. + </doc> + <doc type="scenario"> + Attempt to create a consumers with an empty tag. + </doc> + </exception> + </field> + + <field name="no-local" type="bit"> + <doc>If the no-local field is set the server will not send messages to the connection that + published them.</doc> + </field> + + <field name="no-ack" type="bit" label="no acknowledgement needed"> + <doc> + If this field is set the server does not expect acknowledgements for messages. That is, + when a message is delivered to the client the server automatically and silently + acknowledges it on behalf of the client. This functionality increases performance but at + the cost of reliability. Messages can get lost if a client dies before it can deliver them + to the application. + </doc> + </field> + + <field name="exclusive" type="bit" label="request exclusive access"> + <doc> + Request exclusive consumer access, meaning only this consumer can access the queue. + </doc> + + <exception name="in-use" error-code="resource-locked"> + <doc> + If the server cannot grant exclusive access to the queue when asked, - because there are + other consumers active - it MUST raise an exception. + </doc> + </exception> + </field> + + <field name="nowait" type="bit" label="do not send a reply command"> + <doc> + If set, the server will not respond to the command. The client should not wait for a reply + command. If the server could not complete the command it will raise an exception. + </doc> + </field> + + <field name="arguments" type="map" label="arguments for consuming"> + <doc> + A set of arguments for the consume. The syntax and semantics of these arguments depends on + the providers implementation. + </doc> + </field> + </command> + + <command name="consume-ok" code="0x4" label="confirm a new consumer"> + <doc> + This command provides the client with a consumer tag which it MUST use in commands that work + with the consumer. + </doc> + + <implement role="client" handle="MUST" /> + + <field name="consumer-tag" type="str8"> + <doc> + Holds the consumer tag specified by the client or provided by the server. + </doc> + </field> + </command> + + <!-- - Command: file.cancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="cancel" code="0x5" label="end a queue consumer"> + <doc> + This command cancels a consumer. This does not affect already delivered messages, but it + does mean the server will not send any more messages for that consumer. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="consumer-tag" type="str8"> + <doc> + the identifier of the consumer to be cancelled. + </doc> + </field> + </command> + + <!-- - Command: file.open - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="open" code="0x6" label="request to start staging"> + <doc> + This command requests permission to start staging a message. Staging means sending the + message into a temporary area at the recipient end and then delivering the message by + referring to this temporary area. Staging is how the protocol handles partial file transfers + - if a message is partially staged and the connection breaks, the next time the sender + starts to stage it, it can restart from where it left off. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <response name="open-ok" /> + + <field name="identifier" type="str8" label="staging identifier"> + <doc> + This is the staging identifier. This is an arbitrary string chosen by the sender. For + staging to work correctly the sender must use the same staging identifier when staging the + same message a second time after recovery from a failure. A good choice for the staging + identifier would be the SHA1 hash of the message properties data (including the original + filename, revised time, etc.). + </doc> + </field> + + <field name="content-size" type="uint64" label="message content size"> + <doc> + The size of the content in octets. The recipient may use this information to allocate or + check available space in advance, to avoid "disk full" errors during staging of very large + messages. + </doc> + + <rule name="content-size"> + <doc> + The sender MUST accurately fill the content-size field. Zero-length content is + permitted. + </doc> + </rule> + </field> + </command> + + <!-- - Command: file.open-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="open-ok" code="0x7" label="confirm staging ready"> + <doc> + This command confirms that the recipient is ready to accept staged data. If the message was + already partially-staged at a previous time the recipient will report the number of octets + already staged. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <response name="stage" /> + + <field name="staged-size" type="uint64" label="already staged amount"> + <doc> + The amount of previously-staged content in octets. For a new message this will be zero. + </doc> + + <rule name="behavior"> + <doc> + The sender MUST start sending data from this octet offset in the message, counting from + zero. + </doc> + </rule> + + <rule name="staging"> + <doc> + The recipient MAY decide how long to hold partially-staged content and MAY implement + staging by always discarding partially-staged content. However if it uses the file + content type it MUST support the staging commands. + </doc> + </rule> + </field> + </command> + + <!-- - Command: file.stage - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="stage" code="0x8" label="stage message content"> + <doc> + This command stages the message, sending the message content to the recipient from the octet + offset specified in the Open-Ok command. + </doc> + + <implement role="server" handle="MUST" /> + <implement role="client" handle="MUST" /> + + <segments> + <header required="true"> + <entry type="file-properties"/> + </header> + <body/> + </segments> + </command> + + <!-- - Command: file.publish - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="publish" code="0x9" label="publish a message"> + <doc> + This command publishes a staged file message to a specific exchange. The file message will + be routed to queues as defined by the exchange configuration and distributed to any active + consumers when the transaction, if any, is committed. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="exchange" type="exchange.name"> + <doc> + Specifies the name of the exchange to publish to. The exchange name can be empty, meaning + the default exchange. If the exchange name is specified, and that exchange does not exist, + the server will raise an exception. + </doc> + + <rule name="default"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + </rule> + + <exception name="refusal" error-code="not-implemented"> + <doc> + The exchange MAY refuse file content in which case it MUST send an exception. + </doc> + </exception> + </field> + + <field name="routing-key" type="str8" label="Message routing key"> + <doc> + Specifies the routing key for the message. The routing key is used for routing messages + depending on the exchange configuration. + </doc> + </field> + + <field name="mandatory" type="bit" label="indicate mandatory routing"> + <doc> + This flag tells the server how to react if the message cannot be routed to a queue. If + this flag is set, the server will return an unroutable message with a Return command. If + this flag is zero, the server silently drops the message. + </doc> + + <rule name="implementation"> + <doc> + The server SHOULD implement the mandatory flag. + </doc> + </rule> + </field> + + <field name="immediate" type="bit" label="request immediate delivery"> + <doc> + This flag tells the server how to react if the message cannot be routed to a queue + consumer immediately. If this flag is set, the server will return an undeliverable message + with a Return command. If this flag is zero, the server will queue the message, but with + no guarantee that it will ever be consumed. + </doc> + + <rule name="implementation"> + <doc> + The server SHOULD implement the immediate flag. + </doc> + </rule> + </field> + + <field name="identifier" type="str8" label="staging identifier"> + <doc> + This is the staging identifier of the message to publish. The message must have been + staged. Note that a client can send the Publish command asynchronously without waiting for + staging to finish. + </doc> + </field> + </command> + + <!-- - Command: file.return - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="return" code="0xa" label="return a failed message"> + <doc> + This command returns an undeliverable message that was published with the "immediate" flag + set, or an unroutable message published with the "mandatory" flag set. The reply code and + text provide information about the reason that the message was undeliverable. + </doc> + + <implement role="client" handle="MUST" /> + + <field name="reply-code" type="return-code" /> + + <field name="reply-text" type="str8" label="The localized reply text."> + <doc> + This text can be logged as an aid to resolving issues. + </doc> + </field> + + <field name="exchange" type="exchange.name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + </doc> + </field> + + <field name="routing-key" type="str8" label="Message routing key"> + <doc> + Specifies the routing key name specified when the message was published. + </doc> + </field> + + <segments> + <header required="true"> + <entry type="file-properties"/> + </header> + <body/> + </segments> + </command> + + <!-- - Command: file.deliver - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="deliver" code="0xb" label="notify the client of a consumer message"> + <doc> + This command delivers a staged file message to the client, via a consumer. In the + asynchronous message delivery model, the client starts a consumer using the consume command, + then the server responds with Deliver commands as and when messages arrive for that + consumer. + </doc> + + <rule name="redelivery-tracking"> + <doc> + The server SHOULD track the number of times a message has been delivered to clients and + when a message is redelivered a certain number of times - e.g. 5 times - without being + acknowledged, the server SHOULD consider the message to be non-processable (possibly + causing client applications to abort), and move the message to a dead letter queue. + </doc> + </rule> + + <implement role="client" handle="MUST" /> + + <field name="consumer-tag" type="str8" /> + + <field name="delivery-tag" type="uint64" > + <doc> + The server-assigned and session-specific delivery tag + </doc> + + <rule name="non-zero"> + <doc> + The server MUST NOT use a zero value for delivery tags. Zero is reserved for client use, + meaning "all messages so far received". + </doc> + </rule> + </field> + + <field name="redelivered" type="bit" label="Indicate possible duplicate delivery"> + <doc> + This boolean flag indicates that the message may have been previously delivered to this + or another client. + </doc> + </field> + + <field name="exchange" type="exchange.name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + </doc> + </field> + + <field name="routing-key" type="str8" label="Message routing key"> + <doc> + Specifies the routing key name specified when the message was published. + </doc> + </field> + + <field name="identifier" type="str8" label="staging identifier"> + <doc> + This is the staging identifier of the message to deliver. The message must have been + staged. Note that a server can send the Deliver command asynchronously without waiting for + staging to finish. + </doc> + </field> + </command> + + <!-- - Command: file.ack - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="ack" code="0xc" label="acknowledge one or more messages"> + <doc> + This command acknowledges one or more messages delivered via the Deliver command. The client + can ask to confirm a single message or a set of messages up to and including a specific + message. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="delivery-tag" type="uint64" > + <doc> + The identifier of the message being acknowledged + </doc> + <rule name="session-local"> + <doc> + The delivery tag is valid only within the session from which the message was received. + i.e. A client MUST NOT receive a message on one session and then acknowledge it on + another. + </doc> + </rule> + </field> + + <field name="multiple" type="bit" label="acknowledge multiple messages"> + <doc> + If set to 1, the delivery tag is treated as "up to and including", so that the client can + acknowledge multiple messages with a single command. If set to zero, the delivery tag + refers to a single message. If the multiple field is 1, and the delivery tag is zero, + tells the server to acknowledge all outstanding messages. + </doc> + + <rule name="validation"> + <doc> + The server MUST validate that a non-zero delivery-tag refers to an delivered message, + and raise an exception if this is not the case. + </doc> + </rule> + </field> + </command> + + <!-- - Command: file.reject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="reject" code="0xd" label="reject an incoming message"> + <doc> + This command allows a client to reject a message. It can be used to return untreatable + messages to their original queue. Note that file content is staged before delivery, so the + client will not use this command to interrupt delivery of a large message. + </doc> + + <rule name="server-interpretation"> + <doc> + The server SHOULD interpret this command as meaning that the client is unable to process + the message at this time. + </doc> + </rule> + + <rule name="not-selection"> + <doc> + A client MUST NOT use this command as a means of selecting messages to process. A rejected + message MAY be discarded or dead-lettered, not necessarily passed to another client. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <field name="delivery-tag" type="uint64"> + <doc> + the identifier of the message to be rejected + </doc> + <rule name="session-local"> + <doc> + The delivery tag is valid only within the session from which the message was received. + i.e. A client MUST NOT receive a message on one session and then reject it on another. + </doc> + </rule> + </field> + + <field name="requeue" type="bit" label="requeue the message"> + <doc> + If this field is zero, the message will be discarded. If this bit is 1, the server will + attempt to requeue the message. + </doc> + + <rule name="requeue-strategy"> + <doc> + The server MUST NOT deliver the message to the same client within the context of the + current session. The recommended strategy is to attempt to deliver the message to an + alternative consumer, and if that is not possible, to move the message to a dead-letter + queue. The server MAY use more sophisticated tracking to hold the message on the queue + and redeliver it to the same client at a later stage. + </doc> + </rule> + </field> + </command> + + </class> + + <!-- == Class: stream ======================================================================== --> + + <class name="stream" code="0xa" label="work with streaming content"> + <doc> + The stream class provides commands that support multimedia streaming. The stream class uses + the following semantics: one message is one packet of data; delivery is unacknowledged and + unreliable; the consumer can specify quality of service parameters that the server can try to + adhere to; lower-priority messages may be discarded in favor of high priority messages. + </doc> + + <doc type="grammar"> + stream = C:QOS S:QOS-OK + / C:CONSUME S:CONSUME-OK + / C:CANCEL + / C:PUBLISH content + / S:RETURN + / S:DELIVER content + </doc> + + <rule name="overflow-discard"> + <doc> + The server SHOULD discard stream messages on a priority basis if the queue size exceeds some + configured limit. + </doc> + </rule> + + <rule name="priority-levels"> + <doc> + The server MUST implement at least 2 priority levels for stream messages, where priorities + 0-4 and 5-9 are treated as two distinct levels. The server MAY implement up to 10 priority + levels. + </doc> + </rule> + + <rule name="acknowledgement-support"> + <doc> + The server MUST implement automatic acknowledgements on stream content. That is, as soon as + a message is delivered to a client via a Deliver command, the server must remove it from the + queue. + </doc> + </rule> + + <role name="server" implement="MAY" /> + <role name="client" implement="MAY" /> + + <!-- These are the properties for a Stream content --> + <struct name="stream-properties" size="4" code="0x1" pack="2"> + <field name="content-type" type="str8" label="MIME content type" /> + <field name="content-encoding" type="str8" label="MIME content encoding" /> + <field name="headers" type="map" label="message header field table" /> + <field name="priority" type="uint8" label="message priority, 0 to 9" /> + <field name="timestamp" type="datetime" label="message timestamp" /> + </struct> + + <domain name="return-code" type="uint16" label="return code from server"> + <doc> + The return code. The AMQP return codes are defined by this enum. + </doc> + <enum> + <choice name="content-too-large" value="311"> + <doc> + The client attempted to transfer content larger than the server could accept. + </doc> + </choice> + + <choice name="no-route" value="312"> + <doc> + The exchange cannot route a message, most likely due to an invalid routing key. Only + when the mandatory flag is set. + </doc> + </choice> + + <choice name="no-consumers" value="313"> + <doc> + The exchange cannot deliver to a consumer when the immediate flag is set. As a result of + pending data on the queue or the absence of any consumers of the queue. + </doc> + </choice> + </enum> + </domain> + + <!-- - Command: stream.qos - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="qos" code="0x1" label="specify quality of service"> + <doc> + This command requests a specific quality of service. The QoS can be specified for the + current session or for all sessions on the connection. The particular properties and + semantics of a qos command always depend on the content class semantics. Though the qos + command could in principle apply to both peers, it is currently meaningful only for the + server. + </doc> + + <implement role="server" handle="MUST" /> + + <response name="qos-ok" /> + + <field name="prefetch-size" type="uint32" label="pre-fetch window in octets"> + <doc> + The client can request that messages be sent in advance so that when the client finishes + processing a message, the following message is already held locally, rather than needing + to be sent within the session. Pre-fetching gives a performance improvement. This field + specifies the pre-fetch window size in octets. May be set to zero, meaning "no specific + limit". Note that other pre-fetch limits may still apply. + </doc> + </field> + + <field name="prefetch-count" type="uint16" label="pre-fetch window in messages"> + <doc> + Specifies a pre-fetch window in terms of whole messages. This field may be used in + combination with the prefetch-size field; a message will only be sent in advance if both + pre-fetch windows (and those at the session and connection level) allow it. + </doc> + </field> + + <field name="consume-rate" type="uint32" label="transfer rate in octets/second"> + <doc> + Specifies a desired transfer rate in octets per second. This is usually determined by the + application that uses the streaming data. A value of zero means "no limit", i.e. as + rapidly as possible. + </doc> + + <rule name="ignore-prefetch"> + <doc> + The server MAY ignore the pre-fetch values and consume rates, depending on the type of + stream and the ability of the server to queue and/or reply it. + </doc> + </rule> + + <rule name="drop-by-priority"> + <doc> + The server MAY drop low-priority messages in favor of high-priority messages. + </doc> + </rule> + </field> + + <field name="global" type="bit" label="apply to entire connection"> + <doc> + By default the QoS settings apply to the current session only. If this field is set, they + are applied to the entire connection. + </doc> + </field> + </command> + + <!-- - Command: stream.qos-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="qos-ok" code="0x2" label="confirm the requested qos"> + <doc> + This command tells the client that the requested QoS levels could be handled by the server. + The requested QoS applies to all active consumers until a new QoS is defined. + </doc> + + <implement role="client" handle="MUST" /> + </command> + + <!-- - Command: stream.consume - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="consume" code="0x3" label="start a queue consumer"> + <doc> + This command asks the server to start a "consumer", which is a transient request for + messages from a specific queue. Consumers last as long as the session they were created on, + or until the client cancels them. + </doc> + + <rule name="min-consumers"> + <doc> + The server SHOULD support at least 16 consumers per queue, unless the queue was declared + as private, and ideally, impose no limit except as defined by available resources. + </doc> + </rule> + + <rule name="priority-based-delivery"> + <doc> + Streaming applications SHOULD use different sessions to select different streaming + resolutions. AMQP makes no provision for filtering and/or transforming streams except on + the basis of priority-based selective delivery of individual messages. + </doc> + </rule> + + <implement role="server" handle="MUST" /> + + <response name="consume-ok" /> + + <field name="queue" type="queue.name"> + <doc> + Specifies the name of the queue to consume from. + </doc> + + <exception name="queue-exists-if-empty" error-code="not-allowed"> + <doc> + If the queue name in this command is empty, the server MUST raise an exception. + </doc> + </exception> + </field> + + <field name="consumer-tag" type="str8"> + <doc> + Specifies the identifier for the consumer. The consumer tag is local to a connection, so + two clients can use the same consumer tags. + </doc> + + <exception name="not-existing-consumer" error-code="not-allowed"> + <doc> + The tag MUST NOT refer to an existing consumer. If the client attempts to create two + consumers with the same non-empty tag the server MUST raise an exception. + </doc> + </exception> + + <exception name="not-empty-consumer-tag" error-code="not-allowed"> + <doc> + The client MUST NOT specify a tag that is empty or blank. + </doc> + <doc type="scenario"> + Attempt to create a consumers with an empty tag. + </doc> + </exception> + </field> + + <field name="no-local" type="bit"> + <doc>If the no-local field is set the server will not send messages to the connection that + published them.</doc> + </field> + + <field name="exclusive" type="bit" label="request exclusive access"> + <doc> + Request exclusive consumer access, meaning only this consumer can access the queue. + </doc> + + <exception name="in-use" error-code="resource-locked"> + <doc> + If the server cannot grant exclusive access to the queue when asked, - because there are + other consumers active - it MUST raise an exception with return code 405 + (resource locked). + </doc> + </exception> + </field> + + <field name="nowait" type="bit" label="do not send a reply command"> + <doc> + If set, the server will not respond to the command. The client should not wait for a reply + command. If the server could not complete the command it will raise an exception. + </doc> + </field> + + <field name="arguments" type="map" label="arguments for consuming"> + <doc> + A set of arguments for the consume. The syntax and semantics of these arguments depends on + the providers implementation. + </doc> + </field> + </command> + + <!-- - Command: stream.consume-ok - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="consume-ok" code="0x4" label="confirm a new consumer"> + <doc> + This command provides the client with a consumer tag which it may use in commands that work + with the consumer. + </doc> + + <implement role="client" handle="MUST" /> + + <field name="consumer-tag" type="str8"> + <doc> + Holds the consumer tag specified by the client or provided by the server. + </doc> + </field> + </command> + + <!-- - Command: stream.cancel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="cancel" code="0x5" label="end a queue consumer"> + <doc> + This command cancels a consumer. Since message delivery is asynchronous the client may + continue to receive messages for a short while after cancelling a consumer. It may process + or discard these as appropriate. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="consumer-tag" type="str8" /> + </command> + + <!-- - Command: stream.publish - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="publish" code="0x6" label="publish a message"> + <doc> + This command publishes a message to a specific exchange. The message will be routed to + queues as defined by the exchange configuration and distributed to any active consumers as + appropriate. + </doc> + + <implement role="server" handle="MUST" /> + + <field name="exchange" type="exchange.name"> + <doc> + Specifies the name of the exchange to publish to. The exchange name can be empty, meaning + the default exchange. If the exchange name is specified, and that exchange does not exist, + the server will raise an exception. + </doc> + + <rule name="default"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + </rule> + + <exception name="refusal" error-code="not-implemented"> + <doc> + The exchange MAY refuse stream content in which case it MUST respond with an exception. + </doc> + </exception> + </field> + + <field name="routing-key" type="str8" label="Message routing key"> + <doc> + Specifies the routing key for the message. The routing key is used for routing messages + depending on the exchange configuration. + </doc> + </field> + + <field name="mandatory" type="bit" label="indicate mandatory routing"> + <doc> + This flag tells the server how to react if the message cannot be routed to a queue. If + this flag is set, the server will return an unroutable message with a Return command. If + this flag is zero, the server silently drops the message. + </doc> + + <rule name="implementation"> + <doc> + The server SHOULD implement the mandatory flag. + </doc> + </rule> + </field> + + <field name="immediate" type="bit" label="request immediate delivery"> + <doc> + This flag tells the server how to react if the message cannot be routed to a queue + consumer immediately. If this flag is set, the server will return an undeliverable message + with a Return command. If this flag is zero, the server will queue the message, but with + no guarantee that it will ever be consumed. + </doc> + + <rule name="implementation"> + <doc> + The server SHOULD implement the immediate flag. + </doc> + </rule> + </field> + + <segments> + <header required="true"> + <entry type="stream-properties"/> + </header> + <body/> + </segments> + </command> + + <!-- - Command: stream.return - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="return" code="0x7" label="return a failed message"> + <doc> + This command returns an undeliverable message that was published with the "immediate" flag + set, or an unroutable message published with the "mandatory" flag set. The reply code and + text provide information about the reason that the message was undeliverable. + </doc> + + <implement role="client" handle="MUST" /> + + <field name="reply-code" type="return-code" /> + + <field name="reply-text" type="str8" label="The localized reply text."> + <doc> + The localized reply text. This text can be logged as an aid to resolving issues. + </doc> + </field> + + <field name="exchange" type="exchange.name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + </doc> + </field> + + <field name="routing-key" type="str8" label="Message routing key"> + <doc> + Specifies the routing key name specified when the message was published. + </doc> + </field> + + <segments> + <header required="true"> + <entry type="stream-properties"/> + </header> + <body/> + </segments> + </command> + + <!-- - Command: stream.deliver - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <command name="deliver" code="0x8" label="notify the client of a consumer message"> + <doc> + This command delivers a message to the client, via a consumer. In the asynchronous message + delivery model, the client starts a consumer using the Consume command, then the server + responds with Deliver commands as and when messages arrive for that consumer. + </doc> + + <implement role="client" handle="MUST" /> + + <field name="consumer-tag" type="str8" /> + + <field name="delivery-tag" type="uint64"> + <doc> + The server-assigned and session-specific delivery tag + </doc> + <rule name="session-local"> + <doc> + The delivery tag is valid only within the session from which the message was received. + i.e. A client MUST NOT receive a message on one session and then acknowledge it on + another. + </doc> + </rule> + </field> + + <field name="exchange" type="exchange.name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + </doc> + </field> + + <field name="queue" type="queue.name" required="true"> + <doc> + Specifies the name of the queue that the message came from. Note that a single session can + start many consumers on different queues. + </doc> + </field> + + <segments> + <header required="true"> + <entry type="stream-properties"/> + </header> + <body/> + </segments> + </command> + + </class> + +</amqp> diff --git a/qpid/ruby/lib/qpid/specs/amqp.0-10.dtd b/qpid/ruby/lib/qpid/specs/amqp.0-10.dtd new file mode 100644 index 0000000000..2be198525a --- /dev/null +++ b/qpid/ruby/lib/qpid/specs/amqp.0-10.dtd @@ -0,0 +1,246 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + Copyright Notice + ================ + (c) Copyright Cisco Systems, Credit Suisse, Deutsche Börse Systems, Envoy Technologies, Inc., + Goldman Sachs, IONA Technologies PLC, iMatix Corporation sprl.,JPMorgan Chase Bank Inc. N.A, + Novell, Rabbit Technologies Ltd., Red Hat, Inc., TWIST Process Innovations ltd, and 29West Inc + 2006, 2007. All rights reserved. + + License + ======= + JPMorgan Chase Bank & Co., Cisco Systems, Inc., Envoy Technologies Inc., iMatix Corporation, IONA + Technologies, Red Hat, Inc., TWIST Process Innovations, and 29West Inc. (collectively, the + "Authors") each hereby grants to you a worldwide, perpetual, royalty-free, nontransferable, + nonexclusive license to (i) copy, display, distribute and implement the Advanced Messaging Queue + Protocol ("AMQP") Specification and (ii) the Licensed Claims that are held by the Authors, all for + the purpose of implementing the Advanced Messaging Queue Protocol Specification. Your license and + any rights under this Agreement will terminate immediately without notice from any Author if you + bring any claim, suit, demand, or action related to the Advanced Messaging Queue Protocol + Specification against any Author. Upon termination, you shall destroy all copies of the Advanced + Messaging Queue Protocol Specification in your possession or control. + + As used hereunder, "Licensed Claims" means those claims of a patent or patent application, + throughout the world, excluding design patents and design registrations, owned or controlled, or + that can be sublicensed without fee and in compliance with the requirements of this Agreement, by + an Author or its affiliates now or at any future time and which would necessarily be infringed by + implementation of the Advanced Messaging Queue Protocol Specification. A claim is necessarily + infringed hereunder only when it is not possible to avoid infringing it because there is no + plausible non-infringing alternative for implementing the required portions of the Advanced + Messaging Queue Protocol Specification. Notwithstanding the foregoing, Licensed Claims shall not + include any claims other than as set forth above even if contained in the same patent as Licensed + Claims; or that read solely on any implementations of any portion of the Advanced Messaging Queue + Protocol Specification that are not required by the Advanced Messaging Queue Protocol + Specification, or that, if licensed, would require a payment of royalties by the licensor to + unaffiliated third parties. Moreover, Licensed Claims shall not include (i) any enabling + technologies that may be necessary to make or use any Licensed Product but are not themselves + expressly set forth in the Advanced Messaging Queue Protocol Specification (e.g., semiconductor + manufacturing technology, compiler technology, object oriented technology, networking technology, + operating system technology, and the like); or (ii) the implementation of other published + standards developed elsewhere and merely referred to in the body of the Advanced Messaging Queue + Protocol Specification, or (iii) any Licensed Product and any combinations thereof the purpose or + function of which is not required for compliance with the Advanced Messaging Queue Protocol + Specification. For purposes of this definition, the Advanced Messaging Queue Protocol + Specification shall be deemed to include both architectural and interconnection requirements + essential for interoperability and may also include supporting source code artifacts where such + architectural, interconnection requirements and source code artifacts are expressly identified as + being required or documentation to achieve compliance with the Advanced Messaging Queue Protocol + Specification. + + As used hereunder, "Licensed Products" means only those specific portions of products (hardware, + software or combinations thereof) that implement and are compliant with all relevant portions of + the Advanced Messaging Queue Protocol Specification. + + The following disclaimers, which you hereby also acknowledge as to any use you may make of the + Advanced Messaging Queue Protocol Specification: + + THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION IS PROVIDED "AS IS," AND THE AUTHORS MAKE NO + REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS + OF THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION ARE SUITABLE FOR ANY PURPOSE; NOR THAT THE + IMPLEMENTATION OF THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION WILL NOT INFRINGE ANY THIRD + PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + THE AUTHORS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF OR RELATING TO ANY USE, IMPLEMENTATION OR OF THE ADVANCED + MESSAGING QUEUE PROTOCOL SPECIFICATION. + + The name and trademarks of the Authors may NOT be used in any manner, including advertising or + publicity pertaining to the Advanced Messaging Queue Protocol Specification or its contents + without specific, written prior permission. Title to copyright in the Advanced Messaging Queue + Protocol Specification will at all times remain with the Authors. + + No other rights are granted by implication, estoppel or otherwise. + + Upon termination of your license or rights under this Agreement, you shall destroy all copies of + the Advanced Messaging Queue Protocol Specification in your possession or control. + + Trademarks + ========== + "JPMorgan", "JPMorgan Chase", "Chase", the JPMorgan Chase logo and the Octagon Symbol are + trademarks of JPMorgan Chase & Co. + + IMATIX and the iMatix logo are trademarks of iMatix Corporation sprl. + + IONA, IONA Technologies, and the IONA logos are trademarks of IONA Technologies PLC and/or its + subsidiaries. + + LINUX is a trademark of Linus Torvalds. RED HAT and JBOSS are registered trademarks of Red Hat, + Inc. in the US and other countries. + + Java, all Java-based trademarks and OpenOffice.org are trademarks of Sun Microsystems, Inc. in the + United States, other countries, or both. + + Other company, product, or service names may be trademarks or service marks of others. + + Links to full AMQP specification: + ================================= + http://www.envoytech.org/spec/amq/ + http://www.iona.com/opensource/amqp/ + http://www.redhat.com/solutions/specifications/amqp/ + http://www.twiststandards.org/tiki-index.php?page=AMQ + http://www.imatix.com/amqp +--> + +<!ELEMENT amqp (doc|type|struct|domain|constant|class)*> +<!ATTLIST amqp + xmlns CDATA #IMPLIED + major CDATA #REQUIRED + minor CDATA #REQUIRED + port CDATA #REQUIRED + comment CDATA #IMPLIED +> + +<!ELEMENT constant (doc|rule)*> +<!ATTLIST constant + name CDATA #REQUIRED + value CDATA #REQUIRED + label CDATA #IMPLIED +> + +<!ELEMENT type (doc|rule)*> +<!ATTLIST type + name CDATA #REQUIRED + label CDATA #IMPLIED + code CDATA #IMPLIED + fixed-width CDATA #IMPLIED + variable-width CDATA #IMPLIED +> + +<!ELEMENT domain (doc|rule|enum)*> +<!ATTLIST domain + name CDATA #REQUIRED + type CDATA #IMPLIED + label CDATA #IMPLIED +> + +<!ELEMENT struct (field|doc|rule)*> +<!ATTLIST struct + name CDATA #REQUIRED + label CDATA #IMPLIED + size (0|1|2|4) #IMPLIED + pack (0|1|2|4) #IMPLIED + code CDATA #IMPLIED> + +<!ELEMENT enum (choice)*> + +<!ELEMENT choice (doc|rule)*> +<!ATTLIST choice + name CDATA #REQUIRED + value CDATA #REQUIRED +> + +<!ELEMENT class (doc|role|rule|struct|domain|control|command)*> +<!ATTLIST class + name CDATA #REQUIRED + code CDATA #REQUIRED + label CDATA #IMPLIED +> + +<!ELEMENT role (doc|rule)*> +<!ATTLIST role + name CDATA #REQUIRED + implement (MAY|SHOULD|MUST) #REQUIRED +> + +<!ELEMENT control (doc|implement|rule|field|response)*> +<!ATTLIST control + name CDATA #REQUIRED + code CDATA #REQUIRED + label CDATA #IMPLIED +> + +<!ELEMENT command ((doc|implement|rule|exception|field|response)*, result?, segments?)> +<!ATTLIST command + name CDATA #REQUIRED + code CDATA #REQUIRED + label CDATA #IMPLIED +> + +<!ELEMENT implement (doc|rule)*> +<!ATTLIST implement + role CDATA #REQUIRED + handle (MAY|SHOULD|MUST) #REQUIRED + send (MAY|SHOULD|MUST) #IMPLIED +> + +<!ELEMENT field (doc|rule|exception)*> +<!ATTLIST field + name CDATA #REQUIRED + type CDATA #IMPLIED + default CDATA #IMPLIED + code CDATA #IMPLIED + label CDATA #IMPLIED + required CDATA #IMPLIED +> + +<!ELEMENT rule (doc*)> +<!ATTLIST rule + name CDATA #REQUIRED + label CDATA #IMPLIED +> + +<!ELEMENT exception (doc*)> +<!ATTLIST exception + name CDATA #REQUIRED + error-code CDATA #IMPLIED + label CDATA #IMPLIED +> + +<!ELEMENT response (doc|rule)*> +<!ATTLIST response + name CDATA #IMPLIED +> + +<!ELEMENT result (doc|rule|struct)*> +<!ATTLIST result + type CDATA #IMPLIED +> + +<!ELEMENT segments (doc|rule|header|body)*> + +<!ELEMENT header (doc|rule|entry)*> +<!ATTLIST header + required (true|false) #IMPLIED +> + +<!ELEMENT entry (doc|rule)*> +<!ATTLIST entry + type CDATA #REQUIRED +> + +<!ELEMENT body (doc|rule)*> +<!ATTLIST body + required (true|false) #IMPLIED +> + +<!ELEMENT doc (#PCDATA|xref)*> +<!ATTLIST doc + type (grammar|scenario|picture|bnf|todo) #IMPLIED + title CDATA #IMPLIED +> + +<!ELEMENT xref (#PCDATA)> +<!ATTLIST xref + ref CDATA #REQUIRED> diff --git a/qpid/ruby/lib/qpid/test.rb b/qpid/ruby/lib/qpid/test.rb new file mode 100644 index 0000000000..2e643f4348 --- /dev/null +++ b/qpid/ruby/lib/qpid/test.rb @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 "qpid/spec08" +require "qpid/client" + +module Qpid08 + + module Test + + def connect() + spec = Spec.load("../specs/amqp.0-8.xml") + c = Client.new("0.0.0.0", 5672, spec) + c.start({"LOGIN" => "guest", "PASSWORD" => "guest"}) + return c + end + + end + +end diff --git a/qpid/ruby/lib/qpid/traverse.rb b/qpid/ruby/lib/qpid/traverse.rb new file mode 100644 index 0000000000..67358a7eb1 --- /dev/null +++ b/qpid/ruby/lib/qpid/traverse.rb @@ -0,0 +1,64 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +class Object + + public + + def traverse() + traverse! {|o| yield(o); o} + end + + def traverse_children!() + instance_variables.each {|v| + value = instance_variable_get(v) + replacement = yield(value) + instance_variable_set(v, replacement) unless replacement.equal? value + } + end + + def traverse!(replacements = {}) + return replacements[__id__] if replacements.has_key? __id__ + replacement = yield(self) + replacements[__id__] = replacement + traverse_children! {|o| o.traverse!(replacements) {|c| yield(c)}} + return replacement + end + +end + +class Array + def traverse_children!() + map! {|o| yield(o)} + end +end + +class Hash + def traverse_children!() + mods = {} + each_pair {|k, v| + key = yield(k) + value = yield(v) + mods[key] = value unless key.equal? k and value.equal? v + delete(k) unless key.equal? k + } + + merge!(mods) + end +end diff --git a/qpid/ruby/lib/qpid/util.rb b/qpid/ruby/lib/qpid/util.rb new file mode 100644 index 0000000000..2dbc37da09 --- /dev/null +++ b/qpid/ruby/lib/qpid/util.rb @@ -0,0 +1,75 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 'thread' +require 'monitor' + +# Monkeypatch +class MonitorMixin::ConditionVariable + + # Wait until BLOCK returns TRUE or TIMEOUT seconds have passed + # Return TRUE if BLOCK returned TRUE within the TIMEOUT, FALSE + # otherswise + def wait_for(timeout=nil, &block) + start = Time.now + passed = 0 + until yield + if timeout.nil? + wait + elsif passed < timeout + wait(timeout) + else + return false + end + passed = Time.now - start + end + return true + end +end + +module Qpid::Util + + # Similar to Python's threading.Event + class Event + def initialize + @monitor = Monitor.new + @cond = @monitor.new_cond + @set = false + end + + def set + @monitor.synchronize do + @set = true + @cond.signal + end + end + + def clear + @monitor.synchronize { @set = false } + end + + def wait(timeout = nil) + @monitor.synchronize do + unless @set + @cond.wait_for(timeout) { @set } + end + end + end + end +end |