Module: Ticketing::Protocol
- Defined in:
- lib/ticketing/protocol.rb
Overview
Wire encoding and decoding for the binary ticket-lock protocol. The byte layout matches every other client exactly.
Defined Under Namespace
Constant Summary collapse
- OP_ACQUIRE =
"A".ord
- OP_RELEASE =
"R".ord
- NL =
"\n".ord
- MAX_KEY_LEN =
128- MAX_LINE_LEN =
192
Class Method Summary collapse
-
.acquire_frame(key, wait, lease) ⇒ Object
Builds an ACQUIRE frame: 'A' + u16 wait + u16 lease (big-endian, whole seconds) + UTF-8 key + '\n'.
- .be_long(bytes, off) ⇒ Object
- .ceil_secs(duration) ⇒ Object
- .clamp_u16(value) ⇒ Object
-
.parse_reply(body) ⇒ Object
Decodes a reply body (op + fixed + tail, newline already stripped) into an Incoming, or
nilif the op is unknown or the body is malformed. -
.read_frame(input) ⇒ Object
Reads one framed message from
input: a one-byte op, then the fixed bytes for that op (viafixed_for), then a tail terminated by '\n'. -
.release_frame(key) ⇒ Object
Builds a RELEASE frame: 'R' + UTF-8 key + '\n'.
-
.reply_fixed(op) ⇒ Object
Number of fixed bytes that follow the op byte of a reply.
- .utf8_bytes(str) ⇒ Object
-
.validate_key(key) ⇒ Object
Rejects a key that is empty, over MAX_KEY_LEN bytes, or contains a space, carriage return, or newline.
Class Method Details
.acquire_frame(key, wait, lease) ⇒ Object
Builds an ACQUIRE frame: 'A' + u16 wait + u16 lease (big-endian, whole seconds) + UTF-8 key + '\n'. Sub-second durations round up; lease is at least one second.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/ticketing/protocol.rb', line 63 def acquire_frame(key, wait, lease) kb = utf8_bytes(key) w = clamp_u16(ceil_secs(wait)) l = clamp_u16([ceil_secs(lease), 1].max) out = String.new(encoding: Encoding::BINARY) out << OP_ACQUIRE out << ((w >> 8) & 0xFF) out << (w & 0xFF) out << ((l >> 8) & 0xFF) out << (l & 0xFF) out << kb out << NL out end |
.be_long(bytes, off) ⇒ Object
158 159 160 161 162 |
# File 'lib/ticketing/protocol.rb', line 158 def be_long(bytes, off) v = 0 8.times { |i| v = (v << 8) | bytes.getbyte(off + i) } v end |
.ceil_secs(duration) ⇒ Object
164 165 166 167 168 |
# File 'lib/ticketing/protocol.rb', line 164 def ceil_secs(duration) return 0 if duration.nil? || duration <= 0 duration.ceil end |
.clamp_u16(value) ⇒ Object
170 171 172 173 174 175 |
# File 'lib/ticketing/protocol.rb', line 170 def clamp_u16(value) return 0 if value < 0 return 0xFFFF if value > 0xFFFF value end |
.parse_reply(body) ⇒ Object
Decodes a reply body (op + fixed + tail, newline already stripped) into an
Incoming, or nil if the op is unknown or the body is malformed.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/ticketing/protocol.rb', line 90 def parse_reply(body) return nil if body.bytesize.zero? op = body.getbyte(0) tail = body.byteslice(1, body.bytesize - 1) || "".b case op when "A".ord return nil if tail.bytesize < 8 token = be_long(tail, 0) Incoming::Acquired.new(token, tail.byteslice(8, tail.bytesize - 8).to_s.force_encoding(Encoding::UTF_8)) when "T".ord then Incoming::TimedOut.new(tail.dup.force_encoding(Encoding::UTF_8)) when "R".ord then Incoming::Released.new(tail.dup.force_encoding(Encoding::UTF_8)) when "N".ord then Incoming::NotFound.new(tail.dup.force_encoding(Encoding::UTF_8)) when "M".ord then Incoming::Moved.new(tail.dup.force_encoding(Encoding::UTF_8)) when "E".ord then Incoming::Error.new(tail.dup.force_encoding(Encoding::UTF_8)) end end |
.read_frame(input) ⇒ Object
Reads one framed message from input: a one-byte op, then the fixed bytes
for that op (via fixed_for), then a tail terminated by '\n'. Returns the
bytes as +op + fixed + tail+ with the newline stripped. A lone '\n' is an
empty heartbeat frame. Over-long lines are drained and reported as
Ticketing::Protocol::Frame::TOO_LONG without closing the connection.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/ticketing/protocol.rb', line 114 def read_frame(input) first = input.getbyte return Frame::EOF if first.nil? return Frame.data("".b) if first == NL op = first fixed = yield(op) head = String.new(encoding: Encoding::BINARY) head << op got = 0 while got < fixed chunk = input.read(fixed - got) return Frame::EOF if chunk.nil? || chunk.bytesize.zero? head << chunk got += chunk.bytesize end tail = String.new(encoding: Encoding::BINARY) too_long = head.bytesize > MAX_LINE_LEN loop do b = input.getbyte return Frame::EOF if b.nil? break if b == NL unless too_long tail << b too_long = true if head.bytesize + tail.bytesize > MAX_LINE_LEN end end return Frame::TOO_LONG if too_long body = String.new(encoding: Encoding::BINARY) body << head body << tail Frame.data(body) end |
.release_frame(key) ⇒ Object
Builds a RELEASE frame: 'R' + UTF-8 key + '\n'.
79 80 81 82 83 84 85 86 |
# File 'lib/ticketing/protocol.rb', line 79 def release_frame(key) kb = utf8_bytes(key) out = String.new(encoding: Encoding::BINARY) out << OP_RELEASE out << kb out << NL out end |
.reply_fixed(op) ⇒ Object
Number of fixed bytes that follow the op byte of a reply.
41 42 43 |
# File 'lib/ticketing/protocol.rb', line 41 def reply_fixed(op) op == OP_ACQUIRE ? 8 : 0 end |
.utf8_bytes(str) ⇒ Object
152 153 154 155 156 |
# File 'lib/ticketing/protocol.rb', line 152 def utf8_bytes(str) str.to_s.encode(Encoding::UTF_8).b rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError str.to_s.b end |
.validate_key(key) ⇒ Object
Rejects a key that is empty, over MAX_KEY_LEN bytes, or contains a space, carriage return, or newline.
47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/ticketing/protocol.rb', line 47 def validate_key(key) bytes = utf8_bytes(key) if bytes.bytesize.zero? || bytes.bytesize > MAX_KEY_LEN raise InvalidInput, "key must be 1..=128 bytes" end bytes.each_byte do |b| if b == 0x20 || b == 0x0A || b == 0x0D raise InvalidInput, "key must not contain spaces or newlines" end end end |