bricolage-streamingload 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/bricolage/nulllogger.rb +20 -0
- data/lib/bricolage/sqsdatasource.rb +120 -44
- data/lib/bricolage/sqsmock.rb +194 -0
- data/lib/bricolage/streamingload/dispatcher.rb +40 -3
- data/lib/bricolage/streamingload/event.rb +24 -2
- data/lib/bricolage/streamingload/loaderservice.rb +4 -2
- data/lib/bricolage/streamingload/objectbuffer.rb +169 -121
- data/lib/bricolage/streamingload/version.rb +1 -1
- data/test/streamingload/test_dispatcher.rb +111 -0
- data/test/test_sqsdatasource.rb +29 -86
- metadata +4 -2
- data/lib/bricolage/sqswrapper.rb +0 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 848b815669c6580505119917a72a4e97833064c2
|
4
|
+
data.tar.gz: 1809410699822e2a60a21407cad1b5814551adcd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84ecbe1e548953cc1f4889eb07e9289101e1ade28e89370dae39462b61d193521ceda25f59d8a0b0f1760b8555cf254abab50cf7ed3d11e70d81c422af2f8b82
|
7
|
+
data.tar.gz: b367bb4faa24e9755bee5dbeaaaf937a317826e8e562466e3264a19563bbfcc877ddba9801ffda4875d21e39feddfcc8e87df81a6433b9cd1c3a2cabcefc0754
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Bricolage
|
4
|
+
# FIXME: should be defined in the Bricolage package
|
5
|
+
class NullLogger
|
6
|
+
def debug(*args) end
|
7
|
+
def debug?() false end
|
8
|
+
def info(*args) end
|
9
|
+
def info?() false end
|
10
|
+
def warn(*args) end
|
11
|
+
def warn?() false end
|
12
|
+
def error(*args) end
|
13
|
+
def error?() false end
|
14
|
+
def exception(*args) end
|
15
|
+
def with_elapsed_time(*args) yield end
|
16
|
+
def elapsed_time(*args) yield end
|
17
|
+
def level() Logger::ERROR end
|
18
|
+
def level=(l) l end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'bricolage/datasource'
|
2
|
-
require 'bricolage/sqswrapper'
|
3
2
|
require 'securerandom'
|
4
3
|
require 'aws-sdk'
|
5
4
|
require 'json'
|
@@ -28,6 +27,10 @@ module Bricolage
|
|
28
27
|
attr_reader :access_key_id
|
29
28
|
attr_reader :secret_access_key
|
30
29
|
|
30
|
+
attr_reader :visibility_timeout
|
31
|
+
attr_reader :max_number_of_messages
|
32
|
+
attr_reader :wait_time_seconds
|
33
|
+
|
31
34
|
def client
|
32
35
|
@client ||= begin
|
33
36
|
c = @noop ? DummySQSClient.new : Aws::SQS::Client.new(region: @region, access_key_id: @access_key_id, secret_access_key: @secret_access_key)
|
@@ -39,22 +42,18 @@ module Bricolage
|
|
39
42
|
# High-Level Polling Interface
|
40
43
|
#
|
41
44
|
|
42
|
-
def
|
45
|
+
def handle_messages(handler:, message_class:)
|
43
46
|
trap_signals
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
if n_msg == 0
|
50
|
-
n_zero += 1
|
51
|
-
else
|
52
|
-
n_zero = 0
|
47
|
+
polling_loop do
|
48
|
+
result = poll or next true
|
49
|
+
msgs = message_class.for_sqs_result(result)
|
50
|
+
msgs.each do |msg|
|
51
|
+
handler.handle(msg)
|
53
52
|
end
|
54
|
-
|
53
|
+
handler.after_message_batch
|
54
|
+
break if terminating?
|
55
|
+
msgs.empty?
|
55
56
|
end
|
56
|
-
delete_message_buffer.flush_force
|
57
|
-
logger.info "shutdown gracefully"
|
58
57
|
end
|
59
58
|
|
60
59
|
def trap_signals
|
@@ -66,6 +65,7 @@ module Bricolage
|
|
66
65
|
initiate_terminate
|
67
66
|
}
|
68
67
|
end
|
68
|
+
private :trap_signals
|
69
69
|
|
70
70
|
def initiate_terminate
|
71
71
|
# No I/O allowed in this method
|
@@ -76,36 +76,72 @@ module Bricolage
|
|
76
76
|
@terminating
|
77
77
|
end
|
78
78
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
def handle_messages(handlers:, message_class:)
|
86
|
-
n_msg = foreach_message(message_class) do |msg|
|
87
|
-
logger.debug "handling message: #{msg.inspect}" if logger.debug?
|
88
|
-
mid = "handle_#{msg.message_type}"
|
89
|
-
# just ignore unknown event to make app migration easy
|
90
|
-
if handlers.respond_to?(mid, true)
|
91
|
-
handlers.__send__(mid, msg)
|
79
|
+
def polling_loop
|
80
|
+
n_failure = 0
|
81
|
+
while true
|
82
|
+
failed = yield
|
83
|
+
if failed
|
84
|
+
n_failure += 1
|
92
85
|
else
|
93
|
-
|
86
|
+
n_failure = 0
|
94
87
|
end
|
88
|
+
insert_handler_wait(n_failure)
|
95
89
|
end
|
96
|
-
n_msg
|
97
90
|
end
|
91
|
+
private :polling_loop
|
98
92
|
|
99
|
-
def
|
93
|
+
def insert_handler_wait(n_failure)
|
94
|
+
sec = 2 ** [n_failure, 6].min # max 64s
|
95
|
+
logger.info "queue wait: sleep #{sec}" if n_failure > 0
|
96
|
+
sleep sec
|
97
|
+
end
|
98
|
+
private :insert_handler_wait
|
99
|
+
|
100
|
+
def poll
|
100
101
|
result = receive_messages()
|
101
102
|
unless result and result.successful?
|
102
103
|
logger.error "ReceiveMessage failed: #{result ? result.error.message : '(result=nil)'}"
|
103
104
|
return nil
|
104
105
|
end
|
105
|
-
logger.info "receive #{result.messages.size} messages"
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
logger.info "receive #{result.messages.size} messages"
|
107
|
+
result
|
108
|
+
end
|
109
|
+
|
110
|
+
class MessageHandler
|
111
|
+
# abstract logger()
|
112
|
+
|
113
|
+
def handle(msg)
|
114
|
+
logger.debug "handling message: #{msg.inspect}" if logger.debug?
|
115
|
+
if handleable?(msg)
|
116
|
+
call_handler_method(msg)
|
117
|
+
else
|
118
|
+
handle_unknown(msg)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def handleable?(msg)
|
123
|
+
respond_to?(handler_method(msg), true)
|
124
|
+
end
|
125
|
+
|
126
|
+
def call_handler_method(msg)
|
127
|
+
__send__(handler_method(msg), msg)
|
128
|
+
end
|
129
|
+
|
130
|
+
def handler_method(msg)
|
131
|
+
"handle_#{msg.message_type}".intern
|
132
|
+
end
|
133
|
+
|
134
|
+
# Unknown message handler.
|
135
|
+
# Feel free to override this method.
|
136
|
+
def handle_unknown(msg)
|
137
|
+
# just ignore unknown message to make app migration easy
|
138
|
+
logger.error "unknown message type: #{msg.message_type.inspect} (message-id: #{msg.message_id})"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Called after each message batch (ReceiveMessage) is processed.
|
142
|
+
# Override this method in subclasses on demand.
|
143
|
+
def after_message_batch
|
144
|
+
end
|
109
145
|
end
|
110
146
|
|
111
147
|
#
|
@@ -122,6 +158,18 @@ module Bricolage
|
|
122
158
|
result
|
123
159
|
end
|
124
160
|
|
161
|
+
def put(msg)
|
162
|
+
send_message(msg)
|
163
|
+
end
|
164
|
+
|
165
|
+
def send_message(msg)
|
166
|
+
client.send_message(
|
167
|
+
queue_url: @url,
|
168
|
+
message_body: { 'Records' => [msg.body] }.to_json,
|
169
|
+
delay_seconds: msg.delay_seconds
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
125
173
|
def delete_message(msg)
|
126
174
|
client.delete_message(
|
127
175
|
queue_url: @url,
|
@@ -133,20 +181,18 @@ module Bricolage
|
|
133
181
|
delete_message_buffer.put(msg)
|
134
182
|
end
|
135
183
|
|
136
|
-
def
|
137
|
-
|
184
|
+
def process_async_delete(now = Time.now)
|
185
|
+
delete_message_buffer.flush(now)
|
138
186
|
end
|
139
187
|
|
140
|
-
def
|
141
|
-
|
188
|
+
def process_async_delete_force
|
189
|
+
delete_message_buffer.flush_force
|
142
190
|
end
|
143
191
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
delay_seconds: msg.delay_seconds
|
149
|
-
)
|
192
|
+
private
|
193
|
+
|
194
|
+
def delete_message_buffer
|
195
|
+
@delete_message_buffer ||= DeleteMessageBuffer.new(client, @url, logger)
|
150
196
|
end
|
151
197
|
|
152
198
|
class DeleteMessageBuffer
|
@@ -256,6 +302,36 @@ module Bricolage
|
|
256
302
|
end # class SQSDataSource
|
257
303
|
|
258
304
|
|
305
|
+
class SQSClientWrapper
|
306
|
+
|
307
|
+
def initialize(sqs, logger:)
|
308
|
+
@sqs = sqs
|
309
|
+
@logger = logger
|
310
|
+
end
|
311
|
+
|
312
|
+
def receive_message(**args)
|
313
|
+
@logger.debug "receive_message(#{args.inspect})"
|
314
|
+
@sqs.receive_message(**args)
|
315
|
+
end
|
316
|
+
|
317
|
+
def send_message(**args)
|
318
|
+
@logger.debug "send_message(#{args.inspect})"
|
319
|
+
@sqs.send_message(**args)
|
320
|
+
end
|
321
|
+
|
322
|
+
def delete_message(**args)
|
323
|
+
@logger.debug "delete_message(#{args.inspect})"
|
324
|
+
@sqs.delete_message(**args)
|
325
|
+
end
|
326
|
+
|
327
|
+
def delete_message_batch(**args)
|
328
|
+
@logger.debug "delete_message_batch(#{args.inspect})"
|
329
|
+
@sqs.delete_message_batch(**args)
|
330
|
+
end
|
331
|
+
|
332
|
+
end # class SQSClientWrapper
|
333
|
+
|
334
|
+
|
259
335
|
class SQSMessage
|
260
336
|
|
261
337
|
SQS_EVENT_SOURCE = 'bricolage:system'
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'bricolage/sqsdatasource'
|
2
|
+
require 'bricolage/nulllogger'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Bricolage
|
6
|
+
|
7
|
+
def SQSDataSource.new_mock(**args)
|
8
|
+
SQSDataSource.new(
|
9
|
+
url: 'http://sqs/000000000000/queue-name',
|
10
|
+
access_key_id: 'access_key_id_1',
|
11
|
+
secret_access_key: 'secret_access_key_1',
|
12
|
+
visibility_timeout: 30
|
13
|
+
).tap {|ds|
|
14
|
+
logger = NullLogger.new
|
15
|
+
#logger = Bricolage::Logger.default
|
16
|
+
ds.__send__(:initialize_base, 'name', nil, logger)
|
17
|
+
ds.instance_variable_set(:@client, SQSMock::Client.new(**args))
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
module SQSMock
|
22
|
+
|
23
|
+
class Client
|
24
|
+
def initialize(queue: [], receive_message: nil, send_message: nil, delete_message: nil, delete_message_batch: nil)
|
25
|
+
@queue = queue # [[record]]
|
26
|
+
@call_history = []
|
27
|
+
|
28
|
+
@receive_message = receive_message || lambda {|**args|
|
29
|
+
msgs = @queue.shift or break ReceiveMessageResponse.successful([])
|
30
|
+
ReceiveMessageResponse.successful(msgs)
|
31
|
+
}
|
32
|
+
|
33
|
+
@send_message = send_message || lambda {|**args|
|
34
|
+
SendMessageResponse.successful
|
35
|
+
}
|
36
|
+
|
37
|
+
@delete_message = delete_message || lambda {|**args|
|
38
|
+
Response.successful
|
39
|
+
}
|
40
|
+
|
41
|
+
@delete_message_batch = delete_message_batch || lambda {|queue_url:, entries:|
|
42
|
+
# Returns success for all requests by default.
|
43
|
+
DeleteMessageBatchResponse.new.tap {|res|
|
44
|
+
entries.each do |ent|
|
45
|
+
res.add_success_for(ent)
|
46
|
+
end
|
47
|
+
}
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Free free to modify this array contents
|
52
|
+
attr_reader :call_history
|
53
|
+
|
54
|
+
def self.def_mock_method(name)
|
55
|
+
define_method(name) {|**args|
|
56
|
+
@call_history.push CallHistory.new(name.intern, args)
|
57
|
+
instance_variable_get("@#{name}").(**args)
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def_mock_method :receive_message
|
62
|
+
def_mock_method :send_message
|
63
|
+
def_mock_method :delete_message
|
64
|
+
def_mock_method :delete_message_batch
|
65
|
+
end
|
66
|
+
|
67
|
+
CallHistory = Struct.new(:name, :args)
|
68
|
+
|
69
|
+
# success/failure only result
|
70
|
+
class Response
|
71
|
+
def Response.successful
|
72
|
+
new(successful: true)
|
73
|
+
end
|
74
|
+
|
75
|
+
def initialize(successful:)
|
76
|
+
@successful = successful
|
77
|
+
end
|
78
|
+
|
79
|
+
def successful?
|
80
|
+
@successful
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class ReceiveMessageResponse < Response
|
85
|
+
def ReceiveMessageResponse.successful(msgs)
|
86
|
+
new(successful: true, messages: msgs)
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(successful:, messages:)
|
90
|
+
super(successful: successful)
|
91
|
+
@messages = messages
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_reader :messages
|
95
|
+
end
|
96
|
+
|
97
|
+
class SendMessageResponse < Response
|
98
|
+
def SendMessageResponse.successful
|
99
|
+
new(successful: true, message_id: "sqs-sent-message-id-#{Message.new_seq}")
|
100
|
+
end
|
101
|
+
|
102
|
+
def initialize(successful:, message_id:)
|
103
|
+
super(successful: successful)
|
104
|
+
@message_id = message_id
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :message_id
|
108
|
+
end
|
109
|
+
|
110
|
+
class DeleteMessageBatchResponse
|
111
|
+
def initialize(successful: [], failed: [])
|
112
|
+
@successful = successful
|
113
|
+
@failed = failed
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :successful
|
117
|
+
attr_reader :failed
|
118
|
+
|
119
|
+
Success = Struct.new(:id)
|
120
|
+
Failure = Struct.new(:id, :sender_fault, :code, :message)
|
121
|
+
|
122
|
+
def add_success_for(ent)
|
123
|
+
@successful.push Success.new(ent[:id])
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_failure_for(ent)
|
127
|
+
@failed.push Failure.new(ent[:id], true, '400', 'some reason')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class Message
|
132
|
+
def Message.s3_object_created_event(url)
|
133
|
+
raise "is not a S3 URL: #{url.inspect}" unless %r<\As3://\w> =~ url
|
134
|
+
bucket, key = url.sub(%r<s3://>, '').split('/', 2)
|
135
|
+
with_body({
|
136
|
+
eventVersion: '2.0',
|
137
|
+
eventSource: 'aws:s3',
|
138
|
+
awsRegion: 'ap-northeast-1',
|
139
|
+
eventTime: Time.now.iso8601,
|
140
|
+
eventName: 'ObjectCreated:Put',
|
141
|
+
s3: {
|
142
|
+
s3SchemaVersion: '1.0',
|
143
|
+
configurationId: 'TestConfig',
|
144
|
+
bucket: {
|
145
|
+
name: bucket,
|
146
|
+
arn: "arn:aws:s3:::#{bucket}"
|
147
|
+
},
|
148
|
+
object: {
|
149
|
+
key: key,
|
150
|
+
size: 1024
|
151
|
+
}
|
152
|
+
}
|
153
|
+
})
|
154
|
+
end
|
155
|
+
|
156
|
+
@seq = 0
|
157
|
+
|
158
|
+
def Message.new_seq
|
159
|
+
@seq += 1
|
160
|
+
@seq
|
161
|
+
end
|
162
|
+
|
163
|
+
def Message.with_body(body)
|
164
|
+
seq = new_seq
|
165
|
+
new(
|
166
|
+
message_id: "sqs-message-id-#{seq}",
|
167
|
+
receipt_handle: "sqs-receipt-handle-#{seq}",
|
168
|
+
body: body
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
def initialize(message_id: nil, receipt_handle: nil, body: nil)
|
173
|
+
@message_id = message_id
|
174
|
+
@receipt_handle = receipt_handle
|
175
|
+
@body = body
|
176
|
+
@body_json = { Records: [body] }.to_json
|
177
|
+
end
|
178
|
+
|
179
|
+
attr_reader :message_id
|
180
|
+
attr_reader :receipt_handle
|
181
|
+
|
182
|
+
def body
|
183
|
+
@body_json
|
184
|
+
end
|
185
|
+
|
186
|
+
# for debug
|
187
|
+
def body_object
|
188
|
+
@body
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end # module SQSMock
|
193
|
+
|
194
|
+
end # module Bricolage
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'bricolage/context'
|
1
2
|
require 'bricolage/exception'
|
2
3
|
require 'bricolage/version'
|
3
4
|
require 'bricolage/sqsdatasource'
|
@@ -15,7 +16,7 @@ module Bricolage
|
|
15
16
|
|
16
17
|
module StreamingLoad
|
17
18
|
|
18
|
-
class Dispatcher
|
19
|
+
class Dispatcher < SQSDataSource::MessageHandler
|
19
20
|
|
20
21
|
def Dispatcher.main
|
21
22
|
opts = DispatcherOptions.new(ARGV)
|
@@ -54,7 +55,6 @@ module Bricolage
|
|
54
55
|
|
55
56
|
Process.daemon(true) if opts.daemon?
|
56
57
|
create_pid_file opts.pid_file_path if opts.pid_file_path
|
57
|
-
dispatcher.set_dispatch_timer
|
58
58
|
dispatcher.event_loop
|
59
59
|
end
|
60
60
|
|
@@ -82,10 +82,24 @@ module Bricolage
|
|
82
82
|
@dispatch_interval = dispatch_interval
|
83
83
|
@dispatch_message_id = nil
|
84
84
|
@logger = logger
|
85
|
+
@checkpoint_requested = false
|
85
86
|
end
|
86
87
|
|
88
|
+
attr_reader :logger
|
89
|
+
|
87
90
|
def event_loop
|
88
|
-
|
91
|
+
set_dispatch_timer
|
92
|
+
@event_queue.handle_messages(handler: self, message_class: Event)
|
93
|
+
@event_queue.process_async_delete_force
|
94
|
+
logger.info "shutdown gracefully"
|
95
|
+
end
|
96
|
+
|
97
|
+
# override
|
98
|
+
def after_message_batch
|
99
|
+
@event_queue.process_async_delete
|
100
|
+
if @checkpoint_requested
|
101
|
+
create_checkpoint
|
102
|
+
end
|
89
103
|
end
|
90
104
|
|
91
105
|
def handle_shutdown(e)
|
@@ -94,6 +108,29 @@ module Bricolage
|
|
94
108
|
@event_queue.delete_message(e)
|
95
109
|
end
|
96
110
|
|
111
|
+
def handle_checkpoint(e)
|
112
|
+
# Delay creating CHECKPOINT after the current message batch,
|
113
|
+
# because any other extra events are already received.
|
114
|
+
@checkpoint_requested = true
|
115
|
+
# Delete this event immediately
|
116
|
+
@event_queue.delete_message(e)
|
117
|
+
end
|
118
|
+
|
119
|
+
def create_checkpoint
|
120
|
+
logger.info "*** Creating checkpoint requested ***"
|
121
|
+
logger.info "Force-flushing all objects..."
|
122
|
+
flush_all_tasks_immediately
|
123
|
+
logger.info "All objects flushed; shutting down..."
|
124
|
+
@event_queue.initiate_terminate
|
125
|
+
end
|
126
|
+
|
127
|
+
def flush_all_tasks_immediately
|
128
|
+
tasks = @object_buffer.flush_tasks_force
|
129
|
+
tasks.each do |task|
|
130
|
+
@task_queue.put task
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
97
134
|
def handle_data(e)
|
98
135
|
unless e.created?
|
99
136
|
@event_queue.delete_message_async(e)
|
@@ -11,6 +11,7 @@ module Bricolage
|
|
11
11
|
when rec['eventName'] == 'shutdown' then ShutdownEvent
|
12
12
|
when rec['eventName'] == 'dispatch' then DispatchEvent
|
13
13
|
when rec['eventName'] == 'flush' then FlushEvent
|
14
|
+
when rec['eventName'] == 'checkpoint' then CheckPointEvent
|
14
15
|
when rec['eventSource'] == 'aws:s3'
|
15
16
|
S3ObjectEvent
|
16
17
|
else
|
@@ -41,7 +42,26 @@ module Bricolage
|
|
41
42
|
|
42
43
|
alias message_type name
|
43
44
|
|
44
|
-
def init_message
|
45
|
+
def init_message(dummy: nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Flushes all tables and shutdown
|
52
|
+
class CheckPointEvent < Event
|
53
|
+
|
54
|
+
def CheckPointEvent.create
|
55
|
+
super name: 'checkpoint'
|
56
|
+
end
|
57
|
+
|
58
|
+
def CheckPointEvent.parse_sqs_record(msg, rec)
|
59
|
+
{}
|
60
|
+
end
|
61
|
+
|
62
|
+
alias message_type name
|
63
|
+
|
64
|
+
def init_message(dummy: nil)
|
45
65
|
end
|
46
66
|
|
47
67
|
end
|
@@ -75,6 +95,7 @@ module Bricolage
|
|
75
95
|
|
76
96
|
end
|
77
97
|
|
98
|
+
|
78
99
|
class DispatchEvent < Event
|
79
100
|
|
80
101
|
def DispatchEvent.create(delay_seconds:)
|
@@ -83,8 +104,9 @@ module Bricolage
|
|
83
104
|
|
84
105
|
alias message_type name
|
85
106
|
|
86
|
-
def init_message(dummy)
|
107
|
+
def init_message(dummy: nil)
|
87
108
|
end
|
109
|
+
|
88
110
|
end
|
89
111
|
|
90
112
|
|
@@ -11,7 +11,7 @@ module Bricolage
|
|
11
11
|
|
12
12
|
module StreamingLoad
|
13
13
|
|
14
|
-
class LoaderService
|
14
|
+
class LoaderService < SQSDataSource::MessageHandler
|
15
15
|
|
16
16
|
def LoaderService.main
|
17
17
|
opts = LoaderServiceOptions.new(ARGV)
|
@@ -76,7 +76,8 @@ module Bricolage
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def event_loop
|
79
|
-
@task_queue.
|
79
|
+
@task_queue.handle_messages(handler: self, message_class: Task)
|
80
|
+
@logger.info "shutdown gracefully"
|
80
81
|
end
|
81
82
|
|
82
83
|
def execute_task_by_id(task_id)
|
@@ -87,6 +88,7 @@ module Bricolage
|
|
87
88
|
@ctl_ds.open {|conn| LoadTask.load(conn, task_id, force: force) }
|
88
89
|
end
|
89
90
|
|
91
|
+
# message handler
|
90
92
|
def handle_streaming_load_v3(task)
|
91
93
|
# 1. Load task detail from table
|
92
94
|
# 2. Skip disabled (sqs message should not have disabled state since it will never be exectuted)
|
@@ -39,6 +39,7 @@ module Bricolage
|
|
39
39
|
|
40
40
|
end
|
41
41
|
|
42
|
+
|
42
43
|
class ObjectBuffer
|
43
44
|
|
44
45
|
include SQLUtils
|
@@ -55,7 +56,7 @@ module Bricolage
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def flush_tasks
|
58
|
-
task_ids
|
59
|
+
task_ids = nil
|
59
60
|
@ctl_ds.open {|conn|
|
60
61
|
conn.transaction {|txn|
|
61
62
|
task_ids = insert_tasks(conn)
|
@@ -65,145 +66,192 @@ module Bricolage
|
|
65
66
|
return task_ids.map {|id| LoadTask.create(task_id: id) }
|
66
67
|
end
|
67
68
|
|
69
|
+
# Flushes all objects of all tables immediately with no
|
70
|
+
# additional conditions, to create "stream checkpoint".
|
71
|
+
def flush_tasks_force
|
72
|
+
task_ids = []
|
73
|
+
@ctl_ds.open {|conn|
|
74
|
+
conn.transaction {|txn|
|
75
|
+
# insert_task_object_mappings may not consume all saved objects
|
76
|
+
# (e.g. there are too many objects for one table), we must create
|
77
|
+
# tasks repeatedly until there are no unassigned objects.
|
78
|
+
until (ids = insert_tasks_force(conn)).empty?
|
79
|
+
insert_task_object_mappings(conn)
|
80
|
+
task_ids.concat ids
|
81
|
+
end
|
82
|
+
}
|
83
|
+
}
|
84
|
+
return task_ids.map {|id| LoadTask.create(task_id: id) }
|
85
|
+
end
|
86
|
+
|
68
87
|
private
|
69
88
|
|
70
89
|
def insert_object(conn, obj)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
90
|
+
suppress_sql_logging {
|
91
|
+
conn.update(<<-EndSQL)
|
92
|
+
insert into strload_objects
|
93
|
+
( object_url
|
94
|
+
, object_size
|
95
|
+
, data_source_id
|
96
|
+
, message_id
|
97
|
+
, event_time
|
98
|
+
, submit_time
|
99
|
+
)
|
100
|
+
select
|
101
|
+
#{s obj.url}
|
102
|
+
, #{obj.size}
|
103
|
+
, #{s obj.data_source_id}
|
104
|
+
, #{s obj.message_id}
|
105
|
+
, '#{obj.event_time}' AT TIME ZONE 'JST'
|
106
|
+
, current_timestamp
|
107
|
+
from
|
108
|
+
strload_tables
|
109
|
+
where
|
110
|
+
data_source_id = #{s obj.data_source_id}
|
111
|
+
;
|
112
|
+
EndSQL
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def insert_tasks_force(conn)
|
117
|
+
insert_tasks(conn, force: true)
|
118
|
+
end
|
119
|
+
|
120
|
+
def insert_tasks(conn, force: false)
|
121
|
+
task_ids = conn.query_values(<<-EndSQL)
|
122
|
+
insert into strload_tasks
|
123
|
+
( task_class
|
124
|
+
, schema_name
|
125
|
+
, table_name
|
81
126
|
, submit_time
|
82
127
|
)
|
83
128
|
select
|
84
|
-
|
85
|
-
,
|
86
|
-
,
|
87
|
-
, #{s obj.message_id}
|
88
|
-
, '#{obj.event_time}' AT TIME ZONE 'JST'
|
129
|
+
'streaming_load_v3'
|
130
|
+
, tbl.schema_name
|
131
|
+
, tbl.table_name
|
89
132
|
, current_timestamp
|
90
133
|
from
|
91
|
-
strload_tables
|
134
|
+
strload_tables tbl
|
135
|
+
|
136
|
+
-- number of objects not assigned to a task for each schema_name.table_name (> 0)
|
137
|
+
inner join (
|
138
|
+
select
|
139
|
+
data_source_id
|
140
|
+
, count(*) as object_count
|
141
|
+
from
|
142
|
+
(
|
143
|
+
select
|
144
|
+
min(object_id) as object_id
|
145
|
+
, object_url
|
146
|
+
from
|
147
|
+
strload_objects
|
148
|
+
group by
|
149
|
+
object_url
|
150
|
+
) uniq_objects
|
151
|
+
inner join strload_objects using (object_id)
|
152
|
+
left outer join strload_task_objects using (object_id)
|
153
|
+
where
|
154
|
+
task_id is null -- not assigned to a task
|
155
|
+
group by
|
156
|
+
data_source_id
|
157
|
+
) obj
|
158
|
+
using (data_source_id)
|
159
|
+
|
160
|
+
-- preceeding task's submit time
|
161
|
+
left outer join (
|
162
|
+
select
|
163
|
+
schema_name
|
164
|
+
, table_name
|
165
|
+
, max(submit_time) as latest_submit_time
|
166
|
+
from
|
167
|
+
strload_tasks
|
168
|
+
group by
|
169
|
+
schema_name, table_name
|
170
|
+
) task
|
171
|
+
using (schema_name, table_name)
|
92
172
|
where
|
93
|
-
|
173
|
+
not tbl.disabled -- not disabled
|
174
|
+
and (
|
175
|
+
#{force ? "true or" : ""} -- Creates tasks with no conditions if forced
|
176
|
+
obj.object_count > tbl.load_batch_size -- batch_size exceeded?
|
177
|
+
or extract(epoch from current_timestamp - latest_submit_time) > load_interval -- load_interval exceeded?
|
178
|
+
or latest_submit_time is null -- no previous tasks?
|
179
|
+
)
|
180
|
+
returning task_id
|
94
181
|
;
|
95
182
|
EndSQL
|
96
|
-
@logger.level = log_level
|
97
|
-
end
|
98
183
|
|
99
|
-
|
100
|
-
|
101
|
-
insert into
|
102
|
-
strload_tasks (task_class, schema_name, table_name, submit_time)
|
103
|
-
select
|
104
|
-
'streaming_load_v3'
|
105
|
-
, tbl.schema_name
|
106
|
-
, tbl.table_name
|
107
|
-
, current_timestamp
|
108
|
-
from
|
109
|
-
strload_tables tbl
|
110
|
-
inner join (
|
111
|
-
select
|
112
|
-
data_source_id
|
113
|
-
, count(*) as object_count
|
114
|
-
from (
|
115
|
-
select
|
116
|
-
min(object_id) as object_id
|
117
|
-
, object_url
|
118
|
-
from
|
119
|
-
strload_objects
|
120
|
-
group by
|
121
|
-
object_url
|
122
|
-
) uniq_objects
|
123
|
-
inner join strload_objects
|
124
|
-
using(object_id)
|
125
|
-
left outer join strload_task_objects
|
126
|
-
using(object_id)
|
127
|
-
where
|
128
|
-
task_id is null -- not assigned to a task
|
129
|
-
group by
|
130
|
-
data_source_id
|
131
|
-
) obj -- number of objects not assigned to a task per schema_name.table_name (won't return zero)
|
132
|
-
using (data_source_id)
|
133
|
-
left outer join (
|
134
|
-
select
|
135
|
-
schema_name
|
136
|
-
, table_name
|
137
|
-
, max(submit_time) as latest_submit_time
|
138
|
-
from
|
139
|
-
strload_tasks
|
140
|
-
group by
|
141
|
-
schema_name, table_name
|
142
|
-
) task -- preceeding task's submit time
|
143
|
-
using(schema_name, table_name)
|
144
|
-
where
|
145
|
-
not tbl.disabled -- not disabled
|
146
|
-
and (
|
147
|
-
obj.object_count > tbl.load_batch_size -- batch_size exceeded?
|
148
|
-
or extract(epoch from current_timestamp - latest_submit_time) > load_interval -- load_interval exceeded?
|
149
|
-
or latest_submit_time is null -- no last task
|
150
|
-
)
|
151
|
-
returning task_id
|
152
|
-
;
|
153
|
-
EndSQL
|
154
|
-
@logger.info "Number of task created: #{vals.size}"
|
155
|
-
vals
|
184
|
+
@logger.info "Number of task created: #{task_ids.size}"
|
185
|
+
task_ids
|
156
186
|
end
|
157
187
|
|
158
188
|
def insert_task_object_mappings(conn)
|
159
189
|
conn.update(<<-EndSQL)
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
190
|
+
insert into strload_task_objects
|
191
|
+
( task_id
|
192
|
+
, object_id
|
193
|
+
)
|
194
|
+
select
|
195
|
+
task_id
|
196
|
+
, object_id
|
197
|
+
from (
|
198
|
+
select
|
199
|
+
row_number() over (partition by task.task_id order by obj.object_id) as object_count
|
200
|
+
, task.task_id
|
201
|
+
, obj.object_id
|
202
|
+
, load_batch_size
|
203
|
+
from
|
204
|
+
(
|
205
|
+
select
|
206
|
+
data_source_id
|
207
|
+
, object_url
|
208
|
+
, min(object_id) as object_id
|
209
|
+
from
|
210
|
+
strload_objects
|
211
|
+
group by
|
212
|
+
1, 2
|
213
|
+
) obj
|
214
|
+
|
215
|
+
-- tasks without objects
|
216
|
+
inner join (
|
217
|
+
select
|
218
|
+
tbl.data_source_id
|
219
|
+
, min(task_id) as task_id -- pick up oldest task
|
220
|
+
, max(load_batch_size) as load_batch_size
|
221
|
+
from
|
222
|
+
strload_tasks
|
223
|
+
inner join strload_tables tbl
|
224
|
+
using (schema_name, table_name)
|
225
|
+
where
|
226
|
+
-- unassigned objects
|
227
|
+
task_id not in (select task_id from strload_task_objects)
|
228
|
+
group by
|
229
|
+
1
|
230
|
+
) task
|
231
|
+
using (data_source_id)
|
232
|
+
|
233
|
+
left outer join strload_task_objects task_obj
|
234
|
+
using (object_id)
|
235
|
+
where
|
236
|
+
task_obj.object_id is null -- unassigned to a task
|
237
|
+
) as t
|
238
|
+
where
|
239
|
+
object_count <= load_batch_size -- limit number of objects assigned to single task
|
240
|
+
;
|
204
241
|
EndSQL
|
205
242
|
end
|
206
243
|
|
244
|
+
def suppress_sql_logging
|
245
|
+
# CLUDGE
|
246
|
+
orig = @logger.level
|
247
|
+
begin
|
248
|
+
@logger.level = Logger::ERROR
|
249
|
+
yield
|
250
|
+
ensure
|
251
|
+
@logger.level = orig
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
207
255
|
end
|
208
256
|
|
209
257
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'bricolage/context'
|
3
|
+
require 'bricolage/sqsdatasource'
|
4
|
+
require 'bricolage/sqsmock'
|
5
|
+
require 'bricolage/streamingload/dispatcher'
|
6
|
+
|
7
|
+
module Bricolage
|
8
|
+
module StreamingLoad
|
9
|
+
|
10
|
+
class TestDispatcher < Test::Unit::TestCase
|
11
|
+
|
12
|
+
test "checkpoint event" do
|
13
|
+
ctx = Context.for_application('.', environment: 'test', logger: NullLogger.new)
|
14
|
+
ctl_ds = ctx.get_data_source('sql', 'dwhctl')
|
15
|
+
|
16
|
+
event_queue = SQSDataSource.new_mock(queue: [
|
17
|
+
# 1st ReceiveMessage
|
18
|
+
[
|
19
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0001.json.gz'),
|
20
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0002.json.gz'),
|
21
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0003.json.gz'),
|
22
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0004.json.gz'),
|
23
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0005.json.gz')
|
24
|
+
],
|
25
|
+
# 2nd ReceiveMessage
|
26
|
+
[
|
27
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0006.json.gz'),
|
28
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0007.json.gz'),
|
29
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0008.json.gz'),
|
30
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0009.json.gz'),
|
31
|
+
SQSMock::Message.new(body: {eventSource: 'bricolage:system', eventName: 'checkpoint'}),
|
32
|
+
SQSMock::Message.s3_object_created_event('s3://test-bucket/testschema.desttable/datafile-0010.json.gz')
|
33
|
+
]
|
34
|
+
])
|
35
|
+
|
36
|
+
task_queue = SQSDataSource.new_mock
|
37
|
+
|
38
|
+
object_buffer = ObjectBuffer.new(
|
39
|
+
control_data_source: ctl_ds,
|
40
|
+
logger: ctx.logger
|
41
|
+
)
|
42
|
+
|
43
|
+
url_patterns = URLPatterns.for_config([
|
44
|
+
{
|
45
|
+
"url" => %r<\As3://test-bucket/testschema\.desttable/datafile-\d{4}\.json\.gz>.source,
|
46
|
+
"schema" => 'testschema',
|
47
|
+
"table" => 'desttable'
|
48
|
+
}
|
49
|
+
])
|
50
|
+
|
51
|
+
dispatcher = Dispatcher.new(
|
52
|
+
event_queue: event_queue,
|
53
|
+
task_queue: task_queue,
|
54
|
+
object_buffer: object_buffer,
|
55
|
+
url_patterns: url_patterns,
|
56
|
+
dispatch_interval: 600,
|
57
|
+
logger: ctx.logger
|
58
|
+
)
|
59
|
+
|
60
|
+
# FIXME: database cleaner
|
61
|
+
ctl_ds.open {|conn|
|
62
|
+
conn.update("truncate strload_tables")
|
63
|
+
conn.update("truncate strload_objects")
|
64
|
+
conn.update("truncate strload_task_objects")
|
65
|
+
conn.update("truncate strload_tasks")
|
66
|
+
conn.update("insert into strload_tables values ('testschema', 'desttable', 'testschema.desttable', 100, 1800, false)")
|
67
|
+
}
|
68
|
+
dispatcher.event_loop
|
69
|
+
|
70
|
+
# Event Queue Call Sequence
|
71
|
+
hst = event_queue.client.call_history
|
72
|
+
assert_equal :send_message, hst[0].name # start flush timer
|
73
|
+
assert_equal :receive_message, hst[1].name
|
74
|
+
assert_equal :delete_message_batch, hst[2].name
|
75
|
+
assert_equal :receive_message, hst[3].name
|
76
|
+
assert_equal :delete_message, hst[4].name # delete checkpoint
|
77
|
+
assert_equal :delete_message_batch, hst[5].name
|
78
|
+
|
79
|
+
# Task Queue Call Sequence
|
80
|
+
hst = task_queue.client.call_history
|
81
|
+
assert_equal :send_message, hst[0].name
|
82
|
+
assert(/streaming_load_v3/ =~ hst[0].args[:message_body])
|
83
|
+
task_id = JSON.load(hst[0].args[:message_body])['Records'][0]['taskId'].to_i
|
84
|
+
assert_not_equal 0, task_id
|
85
|
+
|
86
|
+
# Object Buffer
|
87
|
+
assert_equal [], unassigned_objects(ctl_ds)
|
88
|
+
task = ctl_ds.open {|conn| LoadTask.load(conn, task_id) }
|
89
|
+
assert_equal 'testschema', task.schema
|
90
|
+
assert_equal 'desttable', task.table
|
91
|
+
assert_equal 10, task.object_urls.size
|
92
|
+
end
|
93
|
+
|
94
|
+
def unassigned_objects(ctl_ds)
|
95
|
+
ctl_ds.open {|conn|
|
96
|
+
conn.query_values(<<-EndSQL)
|
97
|
+
select
|
98
|
+
object_url
|
99
|
+
from
|
100
|
+
strload_objects
|
101
|
+
where
|
102
|
+
object_id not in (select object_id from strload_task_objects)
|
103
|
+
;
|
104
|
+
EndSQL
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
data/test/test_sqsdatasource.rb
CHANGED
@@ -1,111 +1,54 @@
|
|
1
1
|
require 'test/unit'
|
2
2
|
require 'bricolage/streamingload/event'
|
3
|
+
require 'bricolage/sqsmock'
|
3
4
|
require 'bricolage/logger'
|
4
5
|
|
5
6
|
module Bricolage
|
6
7
|
|
7
8
|
class TestSQSDataSource < Test::Unit::TestCase
|
8
9
|
|
9
|
-
def new_sqs_ds(mock_client = nil)
|
10
|
-
SQSDataSource.new(
|
11
|
-
url: 'http://sqs/000000000000/queue-name',
|
12
|
-
access_key_id: 'access_key_id_1',
|
13
|
-
secret_access_key: 'secret_access_key_1',
|
14
|
-
visibility_timeout: 30
|
15
|
-
).tap {|ds|
|
16
|
-
logger = NullLogger.new
|
17
|
-
#logger = Bricolage::Logger.default
|
18
|
-
ds.__send__(:initialize_base, 'name', nil, logger)
|
19
|
-
ds.instance_variable_set(:@client, mock_client) if mock_client
|
20
|
-
}
|
21
|
-
end
|
22
|
-
|
23
|
-
class MockSQSClient
|
24
|
-
def initialize(&block)
|
25
|
-
@handler = block
|
26
|
-
end
|
27
|
-
|
28
|
-
def delete_message_batch(**args)
|
29
|
-
@handler.call(args)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
class NullLogger
|
34
|
-
def debug(*args) end
|
35
|
-
def info(*args) end
|
36
|
-
def warn(*args) end
|
37
|
-
def error(*args) end
|
38
|
-
def exception(*args) end
|
39
|
-
def with_elapsed_time(*args) yield end
|
40
|
-
def elapsed_time(*args) yield end
|
41
|
-
end
|
42
|
-
|
43
|
-
def sqs_message(seq)
|
44
|
-
MockSQSMessage.new("message_id_#{seq}", "receipt_handle_#{seq}")
|
45
|
-
end
|
46
|
-
|
47
|
-
MockSQSMessage = Struct.new(:message_id, :receipt_handle)
|
48
|
-
|
49
|
-
class MockSQSResponse
|
50
|
-
def initialize(successful: [], failed: [])
|
51
|
-
@successful = successful
|
52
|
-
@failed = failed
|
53
|
-
end
|
54
|
-
|
55
|
-
attr_reader :successful
|
56
|
-
attr_reader :failed
|
57
|
-
|
58
|
-
Success = Struct.new(:id)
|
59
|
-
Failure = Struct.new(:id, :sender_fault, :code, :message)
|
60
|
-
|
61
|
-
def add_success_for(ent)
|
62
|
-
@successful.push Success.new(ent[:id])
|
63
|
-
end
|
64
|
-
|
65
|
-
def add_failure_for(ent)
|
66
|
-
@failed.push Failure.new(ent[:id], true, '400', 'some reason')
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
10
|
test "#delete_message_async" do
|
71
|
-
messages =
|
72
|
-
|
73
|
-
entries = args[:entries]
|
74
|
-
if entries.size == 3
|
75
|
-
# first time
|
76
|
-
assert_equal messages[0].receipt_handle, entries[0][:receipt_handle]
|
77
|
-
assert_equal messages[1].receipt_handle, entries[1][:receipt_handle]
|
78
|
-
assert_equal messages[2].receipt_handle, entries[2][:receipt_handle]
|
79
|
-
MockSQSResponse.new.tap {|res|
|
80
|
-
res.add_success_for(entries[0])
|
81
|
-
res.add_failure_for(entries[1])
|
82
|
-
res.add_success_for(entries[2])
|
83
|
-
}
|
84
|
-
else
|
85
|
-
# second time
|
86
|
-
MockSQSResponse.new.tap {|res|
|
87
|
-
res.add_success_for(entries[0])
|
88
|
-
}
|
89
|
-
end
|
11
|
+
messages = (0..2).map {|seq|
|
12
|
+
SQSMock::Message.new(message_id: "message_id_#{seq}", receipt_handle: "receipt_handle_#{seq}")
|
90
13
|
}
|
91
|
-
ds =
|
14
|
+
ds = SQSDataSource.new_mock(
|
15
|
+
delete_message_batch: -> (queue_url:, entries:) {
|
16
|
+
if entries.size == 3
|
17
|
+
# first time
|
18
|
+
assert_equal messages[0].receipt_handle, entries[0][:receipt_handle]
|
19
|
+
assert_equal messages[1].receipt_handle, entries[1][:receipt_handle]
|
20
|
+
assert_equal messages[2].receipt_handle, entries[2][:receipt_handle]
|
21
|
+
SQSMock::DeleteMessageBatchResponse.new.tap {|res|
|
22
|
+
res.add_success_for(entries[0])
|
23
|
+
res.add_failure_for(entries[1])
|
24
|
+
res.add_success_for(entries[2])
|
25
|
+
}
|
26
|
+
else
|
27
|
+
# second time
|
28
|
+
SQSMock::DeleteMessageBatchResponse.new.tap {|res|
|
29
|
+
res.add_success_for(entries[0])
|
30
|
+
}
|
31
|
+
end
|
32
|
+
}
|
33
|
+
)
|
34
|
+
|
92
35
|
ds.delete_message_async(messages[0])
|
93
36
|
ds.delete_message_async(messages[1])
|
94
37
|
ds.delete_message_async(messages[2])
|
95
38
|
|
96
39
|
# first flush
|
97
40
|
flush_time = Time.now
|
98
|
-
ds.
|
99
|
-
|
100
|
-
bufent =
|
41
|
+
ds.process_async_delete(flush_time)
|
42
|
+
delete_buf = ds.__send__(:delete_message_buffer)
|
43
|
+
bufent = delete_buf.instance_variable_get(:@buf).values.first
|
101
44
|
assert_equal 'receipt_handle_1', bufent.message.receipt_handle
|
102
45
|
assert_equal 1, bufent.n_failure
|
103
46
|
assert_false bufent.issuable?(flush_time)
|
104
47
|
assert_true bufent.issuable?(flush_time + 180)
|
105
48
|
|
106
49
|
# second flush
|
107
|
-
ds.
|
108
|
-
assert_true
|
50
|
+
ds.process_async_delete(flush_time + 180)
|
51
|
+
assert_true delete_buf.empty?
|
109
52
|
end
|
110
53
|
|
111
54
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bricolage-streamingload
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Minero Aoki
|
@@ -107,9 +107,10 @@ files:
|
|
107
107
|
- README.md
|
108
108
|
- bin/bricolage-streaming-dispatcher
|
109
109
|
- bin/bricolage-streaming-loader
|
110
|
+
- lib/bricolage/nulllogger.rb
|
110
111
|
- lib/bricolage/snsdatasource.rb
|
111
112
|
- lib/bricolage/sqsdatasource.rb
|
112
|
-
- lib/bricolage/
|
113
|
+
- lib/bricolage/sqsmock.rb
|
113
114
|
- lib/bricolage/streamingload/alertinglogger.rb
|
114
115
|
- lib/bricolage/streamingload/dispatcher.rb
|
115
116
|
- lib/bricolage/streamingload/event.rb
|
@@ -122,6 +123,7 @@ files:
|
|
122
123
|
- lib/bricolage/streamingload/urlpatterns.rb
|
123
124
|
- lib/bricolage/streamingload/version.rb
|
124
125
|
- test/all.rb
|
126
|
+
- test/streamingload/test_dispatcher.rb
|
125
127
|
- test/streamingload/test_event.rb
|
126
128
|
- test/test_sqsdatasource.rb
|
127
129
|
homepage: https://github.com/aamine/bricolage-streamingload
|
data/lib/bricolage/sqswrapper.rb
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
|
3
|
-
module Bricolage
|
4
|
-
|
5
|
-
class SQSClientWrapper
|
6
|
-
def initialize(sqs, logger:)
|
7
|
-
@sqs = sqs
|
8
|
-
@logger = logger
|
9
|
-
end
|
10
|
-
|
11
|
-
def receive_message(**args)
|
12
|
-
@logger.debug "receive_message(#{args.inspect})"
|
13
|
-
@sqs.receive_message(**args)
|
14
|
-
end
|
15
|
-
|
16
|
-
def send_message(**args)
|
17
|
-
@logger.debug "send_message(#{args.inspect})"
|
18
|
-
@sqs.send_message(**args)
|
19
|
-
end
|
20
|
-
|
21
|
-
def delete_message(**args)
|
22
|
-
@logger.debug "delete_message(#{args.inspect})"
|
23
|
-
@sqs.delete_message(**args)
|
24
|
-
end
|
25
|
-
|
26
|
-
def delete_message_batch(**args)
|
27
|
-
@logger.debug "delete_message_batch(#{args.inspect})"
|
28
|
-
@sqs.delete_message_batch(**args)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
|
33
|
-
class DummySQSClient
|
34
|
-
def initialize(queue = [])
|
35
|
-
@queue = queue
|
36
|
-
end
|
37
|
-
|
38
|
-
def receive_message(**args)
|
39
|
-
msg_recs = @queue.shift or return EMPTY_RESULT
|
40
|
-
msgs = msg_recs.map {|recs| Message.new({'Records' => recs}.to_json) }
|
41
|
-
Result.new(true, msgs)
|
42
|
-
end
|
43
|
-
|
44
|
-
def send_message(**args)
|
45
|
-
SUCCESS_RESULT
|
46
|
-
end
|
47
|
-
|
48
|
-
def delete_message(**args)
|
49
|
-
SUCCESS_RESULT
|
50
|
-
end
|
51
|
-
|
52
|
-
class Result
|
53
|
-
def initialize(successful, messages = nil)
|
54
|
-
@successful = successful
|
55
|
-
@messages = messages
|
56
|
-
end
|
57
|
-
|
58
|
-
def successful?
|
59
|
-
@successful
|
60
|
-
end
|
61
|
-
|
62
|
-
attr_reader :messages
|
63
|
-
end
|
64
|
-
|
65
|
-
SUCCESS_RESULT = Result.new(true)
|
66
|
-
EMPTY_RESULT = Result.new(true, [])
|
67
|
-
|
68
|
-
class Message
|
69
|
-
def initialize(body)
|
70
|
-
@body = body
|
71
|
-
end
|
72
|
-
|
73
|
-
attr_reader :body
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
end # module Bricolage
|