ticketing 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/PUBLISH.md +1 -1
- data/README.md +33 -1
- data/lib/ticketing/broker.rb +127 -153
- data/lib/ticketing/connection.rb +278 -187
- data/lib/ticketing/errors.rb +13 -56
- data/lib/ticketing/protocol.rb +70 -144
- data/lib/ticketing/version.rb +1 -1
- data/lib/ticketing.rb +10 -8
- data/ticketing.gemspec +1 -1
- metadata +2 -6
- data/.idea/.gitignore +0 -10
- data/.idea/modules.xml +0 -8
- data/.idea/ticketing-ruby.iml +0 -18
- data/.idea/vcs.xml +0 -6
data/lib/ticketing/connection.rb
CHANGED
|
@@ -1,256 +1,347 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
require_relative 'errors'
|
|
7
|
-
require_relative 'protocol'
|
|
3
|
+
require "socket"
|
|
4
|
+
require "openssl"
|
|
8
5
|
|
|
9
6
|
module Ticketing
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def
|
|
18
|
-
|
|
7
|
+
# One-way TLS: the client verifies the server, never the reverse.
|
|
8
|
+
# 단방향 TLS: 클라이언트가 서버만 검증한다.
|
|
9
|
+
module Tls
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# tls: nil(off) | 'system-roots' | 'insecure-skip-verify' | CA PEM path.
|
|
13
|
+
# Returns [ssl_context, verify_host]. 빌드 시 1회 결정.
|
|
14
|
+
def build(tls)
|
|
15
|
+
case tls
|
|
16
|
+
when nil, "off"
|
|
17
|
+
[nil, false]
|
|
18
|
+
when "system-roots"
|
|
19
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
20
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
21
|
+
context.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
|
|
22
|
+
[context, true]
|
|
23
|
+
when "insecure-skip-verify"
|
|
24
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
25
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
26
|
+
[context, false]
|
|
27
|
+
else
|
|
28
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
29
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
30
|
+
store = OpenSSL::X509::Store.new
|
|
31
|
+
begin
|
|
32
|
+
store.add_file(tls.to_s)
|
|
33
|
+
rescue OpenSSL::X509::StoreError => e
|
|
34
|
+
raise TicketError.new(:io, "TLS config load failed: #{tls} (#{e.message})")
|
|
35
|
+
end
|
|
36
|
+
context.cert_store = store
|
|
37
|
+
[context, true]
|
|
38
|
+
end
|
|
19
39
|
end
|
|
20
40
|
end
|
|
21
41
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
class
|
|
25
|
-
|
|
26
|
-
@queue = Queue.new
|
|
27
|
-
end
|
|
42
|
+
# Shared cluster view: addresses + leader hint. Plain ivars are safe under
|
|
43
|
+
# the GVL. 공유 클러스터 뷰(주소 + 리더 힌트) — GVL 하에서 단순 대입은 안전.
|
|
44
|
+
class Topology
|
|
45
|
+
attr_reader :addrs, :leader
|
|
28
46
|
|
|
29
|
-
def
|
|
30
|
-
@
|
|
47
|
+
def initialize(addrs)
|
|
48
|
+
@addrs = addrs
|
|
49
|
+
@leader = -1
|
|
31
50
|
end
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def await
|
|
38
|
-
kind, value = @queue.pop
|
|
39
|
-
raise value if kind == :err
|
|
52
|
+
# Map a `M`(Moved) address to a leader hint. M 주소 → 리더 힌트.
|
|
53
|
+
def set_leader(addr) = @leader = @addrs.index(addr) || -1
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
def clear_leader_if(index)
|
|
56
|
+
@leader = -1 if @leader == index
|
|
42
57
|
end
|
|
43
58
|
end
|
|
44
59
|
|
|
45
|
-
# One
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# (op, key).
|
|
60
|
+
# One server connection: request queue + batching writer thread + reader,
|
|
61
|
+
# with self-healing reconnect. 서버 연결 하나 — 요청 큐 + 배칭 writer 스레드 +
|
|
62
|
+
# reader, 자가 치유 재접속.
|
|
49
63
|
class Conn
|
|
50
|
-
|
|
64
|
+
STATUS_DOWN = 0
|
|
65
|
+
STATUS_UP = 1
|
|
66
|
+
STATUS_DRAINING = 2
|
|
67
|
+
|
|
68
|
+
REQ_QUEUE = 1024
|
|
69
|
+
WRITE_BATCH = 64
|
|
70
|
+
BACKOFF_MIN = 0.1
|
|
71
|
+
BACKOFF_MAX = 3.2
|
|
72
|
+
# A session shorter than this is a failure (auth/crash loop) — back off.
|
|
73
|
+
# 이보다 짧은 세션은 실패로 간주 — 백오프 적용.
|
|
74
|
+
SHORT_SESSION = 1.0
|
|
75
|
+
CONNECT_TIMEOUT = 3
|
|
76
|
+
|
|
77
|
+
DRAIN_REASONS = %w[not_active auth_failed no_leader].freeze
|
|
51
78
|
|
|
52
79
|
attr_reader :addr
|
|
53
80
|
|
|
54
|
-
def initialize(addr, logger)
|
|
81
|
+
def initialize(addr, index, token, ssl_context, verify_host, topo, logger)
|
|
55
82
|
@addr = addr
|
|
83
|
+
@index = index
|
|
84
|
+
@token = token
|
|
85
|
+
@ssl_context = ssl_context
|
|
86
|
+
@verify_host = verify_host
|
|
87
|
+
@topo = topo
|
|
56
88
|
@logger = logger
|
|
57
|
-
@
|
|
58
|
-
@
|
|
59
|
-
@
|
|
60
|
-
|
|
61
|
-
@running = true
|
|
62
|
-
@thread = nil
|
|
63
|
-
@writer_mutex = Mutex.new
|
|
64
|
-
@pending_mutex = Mutex.new
|
|
65
|
-
@acquire_waiters = {}
|
|
66
|
-
@release_waiters = {}
|
|
89
|
+
@status = STATUS_DOWN
|
|
90
|
+
@session = nil
|
|
91
|
+
@ever_connected = false
|
|
92
|
+
Thread.new { run_loop }
|
|
67
93
|
end
|
|
68
94
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
def usable? = @status == STATUS_UP
|
|
96
|
+
|
|
97
|
+
# Give a drained (non-leader) connection another chance. 드레인 연결 부활.
|
|
98
|
+
def revive
|
|
99
|
+
@status = STATUS_UP if @status == STATUS_DRAINING
|
|
72
100
|
end
|
|
73
101
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# connection is not up.
|
|
102
|
+
# Send one request; pop the returned queue for the reply (array) or a
|
|
103
|
+
# TicketError. 요청 전송 — 반환 큐를 pop해 응답/오류 수신.
|
|
77
104
|
def request(op, key, frame)
|
|
78
|
-
|
|
79
|
-
@
|
|
80
|
-
|
|
105
|
+
resolver = Queue.new
|
|
106
|
+
session = @session
|
|
107
|
+
if session.nil?
|
|
108
|
+
resolver << TicketError.aborted
|
|
109
|
+
return resolver
|
|
110
|
+
end
|
|
111
|
+
begin
|
|
112
|
+
session.queue.push([op, key, frame, resolver]) # blocking push = backpressure. 백프레셔.
|
|
113
|
+
rescue ClosedQueueError
|
|
114
|
+
resolver << TicketError.aborted
|
|
115
|
+
return resolver
|
|
116
|
+
end
|
|
117
|
+
session.abort_queued if session.closed
|
|
118
|
+
resolver
|
|
119
|
+
end
|
|
81
120
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def logf(message) = @logger&.warn(message)
|
|
85
124
|
|
|
125
|
+
def run_loop
|
|
126
|
+
backoff = BACKOFF_MIN
|
|
127
|
+
loop do
|
|
128
|
+
healthy = false
|
|
86
129
|
begin
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
130
|
+
socket = dial
|
|
131
|
+
begin
|
|
132
|
+
parser = Protocol::FrameParser.new
|
|
133
|
+
handshake(socket, parser)
|
|
134
|
+
logf("reconnected: #{@addr}") if @ever_connected
|
|
135
|
+
@ever_connected = true
|
|
136
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
137
|
+
run_session(socket, parser)
|
|
138
|
+
healthy = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started >= SHORT_SESSION
|
|
139
|
+
logf("connection lost: #{@addr}")
|
|
140
|
+
ensure
|
|
141
|
+
begin
|
|
142
|
+
socket.close
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
95
145
|
end
|
|
96
146
|
end
|
|
97
|
-
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
logf("connect failed: #{@addr} (#{e.message}) — retry in #{(backoff * 1000).to_i}ms")
|
|
149
|
+
end
|
|
150
|
+
if healthy
|
|
151
|
+
backoff = BACKOFF_MIN
|
|
152
|
+
else
|
|
153
|
+
sleep(backoff)
|
|
154
|
+
backoff = [backoff * 2, BACKOFF_MAX].min
|
|
98
155
|
end
|
|
99
156
|
end
|
|
100
|
-
waiter.await
|
|
101
157
|
end
|
|
102
158
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@
|
|
106
|
-
ever_connected = false
|
|
107
|
-
while @running
|
|
108
|
-
sock = connect_socket
|
|
109
|
-
next if sock.nil?
|
|
159
|
+
def dial
|
|
160
|
+
host, _, port = @addr.rpartition(":")
|
|
161
|
+
raise TicketError.new(:invalid_input, "address must be host:port — #{@addr}") if host.empty?
|
|
110
162
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@logger.warn("reconnected: #{@addr}") if ever_connected
|
|
115
|
-
ever_connected = true
|
|
163
|
+
socket = Socket.tcp(host, Integer(port), connect_timeout: CONNECT_TIMEOUT)
|
|
164
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
165
|
+
return socket if @ssl_context.nil?
|
|
116
166
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
167
|
+
ssl = OpenSSL::SSL::SSLSocket.new(socket, @ssl_context)
|
|
168
|
+
ssl.hostname = host
|
|
169
|
+
ssl.sync_close = true
|
|
170
|
+
ssl.connect
|
|
171
|
+
ssl.post_connection_check(host) if @verify_host
|
|
172
|
+
ssl
|
|
173
|
+
end
|
|
122
174
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
175
|
+
# Answer the `"<hash> <nonce>"` challenge. challenge 응답.
|
|
176
|
+
def handshake(socket, parser)
|
|
177
|
+
line = nil
|
|
178
|
+
while line.nil?
|
|
179
|
+
parser.feed(socket.readpartial(4096))
|
|
180
|
+
line = parser.next_line
|
|
127
181
|
end
|
|
182
|
+
space = line.index(" ")
|
|
183
|
+
raise TicketError.new(:protocol, "bad challenge") if space.nil? || space.zero?
|
|
184
|
+
|
|
185
|
+
socket.write(Protocol.auth_digest(@token, line.byteslice(space + 1..)) + "\n")
|
|
128
186
|
end
|
|
129
187
|
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
188
|
+
# One live session. Teardown aborts every in-flight request so the broker
|
|
189
|
+
# retries elsewhere. 세션 하나 — 종료 시 진행 중 요청 전부 중단.
|
|
190
|
+
def run_session(socket, parser)
|
|
191
|
+
session = Session.new
|
|
192
|
+
@session = session
|
|
193
|
+
@status = STATUS_UP
|
|
194
|
+
writer = Thread.new { write_loop(session, socket) }
|
|
195
|
+
read_loop(session, socket, parser)
|
|
196
|
+
ensure
|
|
197
|
+
session.closed = true
|
|
198
|
+
@session = nil
|
|
199
|
+
@status = STATUS_DOWN
|
|
200
|
+
@topo.clear_leader_if(@index)
|
|
201
|
+
session.queue.close
|
|
135
202
|
begin
|
|
136
|
-
|
|
203
|
+
socket.close
|
|
137
204
|
rescue StandardError
|
|
138
205
|
nil
|
|
139
206
|
end
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
private
|
|
145
|
-
|
|
146
|
-
def waiters(op)
|
|
147
|
-
op == Protocol::OP_ACQUIRE ? @acquire_waiters : @release_waiters
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def connect_socket
|
|
151
|
-
sock = TCPSocket.new(@host, @port)
|
|
152
|
-
sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
153
|
-
sock
|
|
154
|
-
rescue StandardError => e
|
|
155
|
-
@logger.warn("connect failed: #{@addr} (#{e.message}) — retrying in #{RECONNECT_INTERVAL}s")
|
|
156
|
-
sleep RECONNECT_INTERVAL if @running
|
|
157
|
-
nil
|
|
207
|
+
writer&.join(1)
|
|
208
|
+
session.abort_queued
|
|
209
|
+
session.fail_all
|
|
158
210
|
end
|
|
159
211
|
|
|
160
|
-
|
|
212
|
+
# Register pending then write, in queue order — same-key FIFO holds because
|
|
213
|
+
# this is the single consumer. 단일 소비자가 등록→쓰기 순서 보장(같은 키 FIFO).
|
|
214
|
+
def write_loop(session, socket)
|
|
215
|
+
batch = []
|
|
161
216
|
loop do
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
217
|
+
first = session.queue.pop
|
|
218
|
+
return if first.nil? # closed queue. 큐 닫힘.
|
|
219
|
+
|
|
220
|
+
batch.clear
|
|
221
|
+
batch << first
|
|
222
|
+
while batch.size < WRITE_BATCH
|
|
223
|
+
item = begin
|
|
224
|
+
session.queue.pop(true)
|
|
225
|
+
rescue ThreadError
|
|
226
|
+
break
|
|
167
227
|
end
|
|
228
|
+
break if item.nil?
|
|
168
229
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
next if bytes.bytesize.zero?
|
|
176
|
-
|
|
177
|
-
incoming = Protocol.parse_reply(bytes)
|
|
178
|
-
if incoming.nil?
|
|
179
|
-
@logger.error("unrecognized reply from #{@addr}")
|
|
180
|
-
next
|
|
181
|
-
end
|
|
182
|
-
dispatch(incoming)
|
|
230
|
+
batch << item
|
|
231
|
+
end
|
|
232
|
+
data = +"".b
|
|
233
|
+
batch.each do |op, key, frame, resolver|
|
|
234
|
+
session.register(op, key, resolver)
|
|
235
|
+
data << frame
|
|
183
236
|
end
|
|
237
|
+
socket.write(data)
|
|
238
|
+
end
|
|
239
|
+
rescue StandardError
|
|
240
|
+
begin
|
|
241
|
+
socket.close
|
|
242
|
+
rescue StandardError
|
|
243
|
+
nil
|
|
184
244
|
end
|
|
185
245
|
end
|
|
186
246
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
when Protocol::Incoming::TimedOut
|
|
192
|
-
resolve(Protocol::OP_ACQUIRE, incoming.key, Reply::TIMED_OUT)
|
|
193
|
-
when Protocol::Incoming::Released
|
|
194
|
-
resolve(Protocol::OP_RELEASE, incoming.key, Reply::RELEASED)
|
|
195
|
-
when Protocol::Incoming::NotFound
|
|
196
|
-
resolve(Protocol::OP_RELEASE, incoming.key, Reply::NOT_FOUND)
|
|
197
|
-
when Protocol::Incoming::Moved
|
|
198
|
-
@logger.warn("server draining: #{@addr} (moved to #{incoming.addr})")
|
|
199
|
-
@status = :draining
|
|
200
|
-
when Protocol::Incoming::Error
|
|
201
|
-
@logger.error("server error from #{@addr}: #{incoming.reason}")
|
|
202
|
-
if incoming.reason == "not_active"
|
|
203
|
-
@status = :draining
|
|
204
|
-
clear_pending
|
|
247
|
+
def read_loop(session, socket, parser)
|
|
248
|
+
loop do
|
|
249
|
+
while (frame = parser.next_frame)
|
|
250
|
+
handle_reply(session, frame) unless frame.empty?
|
|
205
251
|
end
|
|
252
|
+
parser.feed(socket.readpartial(65_536))
|
|
206
253
|
end
|
|
254
|
+
rescue EOFError, IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
255
|
+
nil
|
|
207
256
|
end
|
|
208
257
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
258
|
+
def handle_reply(session, frame)
|
|
259
|
+
op, text, token = Protocol.parse_reply(frame)
|
|
260
|
+
if op.nil?
|
|
261
|
+
logf("unrecognized reply from #{@addr}")
|
|
262
|
+
return
|
|
263
|
+
end
|
|
264
|
+
case op
|
|
265
|
+
when "A" then session.resolve(Protocol::OP_ACQUIRE, text, [:acquired, token])
|
|
266
|
+
when "T" then session.resolve(Protocol::OP_ACQUIRE, text, [:timed_out, 0])
|
|
267
|
+
when "R" then session.resolve(Protocol::OP_RELEASE, text, [:released, 0])
|
|
268
|
+
when "N" then session.resolve(Protocol::OP_RELEASE, text, [:not_found, 0])
|
|
269
|
+
when "M"
|
|
270
|
+
logf("not leader: #{@addr} (leader is #{text})")
|
|
271
|
+
@topo.set_leader(text)
|
|
272
|
+
drain(session)
|
|
273
|
+
when "E"
|
|
274
|
+
logf("server error from #{@addr}: #{text}")
|
|
275
|
+
if DRAIN_REASONS.include?(text)
|
|
276
|
+
@topo.clear_leader_if(@index) if text == "no_leader"
|
|
277
|
+
drain(session)
|
|
219
278
|
end
|
|
220
|
-
|
|
279
|
+
end
|
|
221
280
|
end
|
|
222
281
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
rescue StandardError
|
|
229
|
-
nil
|
|
230
|
-
end
|
|
231
|
-
clear_pending
|
|
282
|
+
# Stop routing here; abort in-flight so the broker retries elsewhere.
|
|
283
|
+
# 라우팅 중지 + 진행 중 요청 중단 → 브로커가 다른 노드로 재시도.
|
|
284
|
+
def drain(session)
|
|
285
|
+
@status = STATUS_DRAINING
|
|
286
|
+
session.fail_all
|
|
232
287
|
end
|
|
233
288
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
289
|
+
# In-flight requests per (op, key), FIFO — matches server reply order.
|
|
290
|
+
# (op, key)별 FIFO 대기 — 서버 응답 순서와 일치.
|
|
291
|
+
class Session
|
|
292
|
+
attr_reader :queue
|
|
293
|
+
attr_accessor :closed
|
|
294
|
+
|
|
295
|
+
def initialize
|
|
296
|
+
@queue = SizedQueue.new(REQ_QUEUE)
|
|
297
|
+
@closed = false
|
|
298
|
+
@mutex = Mutex.new
|
|
299
|
+
@acquire = {}
|
|
300
|
+
@release = {}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def map(op) = op == Protocol::OP_ACQUIRE ? @acquire : @release
|
|
304
|
+
|
|
305
|
+
def register(op, key, resolver)
|
|
306
|
+
@mutex.synchronize { (map(op)[key] ||= []) << resolver }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def resolve(op, key, reply)
|
|
310
|
+
resolver = @mutex.synchronize do
|
|
311
|
+
pending = map(op)[key]
|
|
312
|
+
next nil if pending.nil? || pending.empty?
|
|
313
|
+
|
|
314
|
+
first = pending.shift
|
|
315
|
+
map(op).delete(key) if pending.empty?
|
|
316
|
+
first
|
|
243
317
|
end
|
|
244
|
-
|
|
245
|
-
|
|
318
|
+
resolver << reply unless resolver.nil?
|
|
319
|
+
end
|
|
246
320
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
321
|
+
# Abort every in-flight request so the broker retries elsewhere.
|
|
322
|
+
# 진행 중 요청 전부 중단 → 브로커가 다른 노드로 재시도.
|
|
323
|
+
def fail_all
|
|
324
|
+
victims = @mutex.synchronize do
|
|
325
|
+
all = (@acquire.values + @release.values).flatten
|
|
326
|
+
@acquire.clear
|
|
327
|
+
@release.clear
|
|
328
|
+
all
|
|
329
|
+
end
|
|
330
|
+
victims.each { |resolver| resolver << TicketError.aborted }
|
|
251
331
|
end
|
|
252
332
|
|
|
253
|
-
|
|
333
|
+
def abort_queued
|
|
334
|
+
loop do
|
|
335
|
+
item = begin
|
|
336
|
+
@queue.pop(true)
|
|
337
|
+
rescue ThreadError
|
|
338
|
+
break
|
|
339
|
+
end
|
|
340
|
+
break if item.nil?
|
|
341
|
+
|
|
342
|
+
item[3] << TicketError.aborted
|
|
343
|
+
end
|
|
344
|
+
end
|
|
254
345
|
end
|
|
255
346
|
end
|
|
256
347
|
end
|
data/lib/ticketing/errors.rb
CHANGED
|
@@ -1,64 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ticketing
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class NotConnectedError < Error
|
|
16
|
-
def initialize(message = "no connected server")
|
|
17
|
-
super
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# The key (or other argument) failed local validation before any network I/O.
|
|
22
|
-
class InvalidInput < Error
|
|
23
|
-
attr_reader :reason
|
|
24
|
-
|
|
25
|
-
def initialize(reason)
|
|
26
|
-
@reason = reason
|
|
27
|
-
super("invalid input: #{reason}")
|
|
4
|
+
# Client error with a machine-checkable kind. 종류를 코드로 판별할 수 있는 오류.
|
|
5
|
+
#
|
|
6
|
+
# Kinds — :timeout (wait 안에 획득 실패) / :not_connected (연결 없음) /
|
|
7
|
+
# :auth (토큰 거부) / :invalid_input (잘못된 입력) / :protocol (규격 외 응답) /
|
|
8
|
+
# :io (전송 오류) / :connection_aborted (장애조치 중단 — 내부 재시도).
|
|
9
|
+
class TicketError < StandardError
|
|
10
|
+
attr_reader :kind
|
|
11
|
+
|
|
12
|
+
def initialize(kind, message)
|
|
13
|
+
super(message)
|
|
14
|
+
@kind = kind
|
|
28
15
|
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# The server sent a reply that does not fit the request that was made.
|
|
32
|
-
class ProtocolError < Error
|
|
33
|
-
attr_reader :reason
|
|
34
16
|
|
|
35
|
-
def
|
|
36
|
-
@reason = reason
|
|
37
|
-
super("protocol error: #{reason}")
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# A socket read or write failed. {#cause_error} carries the underlying error;
|
|
42
|
-
# when it is a {ConnectionClosed} the broker treats the drop as retryable.
|
|
43
|
-
class IoError < Error
|
|
44
|
-
attr_reader :cause_error
|
|
45
|
-
|
|
46
|
-
def initialize(cause)
|
|
47
|
-
@cause_error = cause
|
|
48
|
-
super("io error: #{cause}")
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Marker for a connection that was lost or never established. Wrapped in an
|
|
53
|
-
# {IoError} so a dropped socket can be told apart from a real I/O fault.
|
|
54
|
-
class ConnectionClosed < StandardError
|
|
55
|
-
def initialize(message = "connection closed")
|
|
56
|
-
super
|
|
57
|
-
end
|
|
58
|
-
end
|
|
17
|
+
def self.aborted = new(:connection_aborted, "connection closed")
|
|
59
18
|
|
|
60
|
-
|
|
61
|
-
def self.closed_error
|
|
62
|
-
IoError.new(ConnectionClosed.new)
|
|
19
|
+
def aborted? = kind == :connection_aborted
|
|
63
20
|
end
|
|
64
21
|
end
|