fluent-plugin-gcloud-pubsub-custom-subscriber 1.3.1

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.
@@ -0,0 +1,253 @@
1
+ require 'json'
2
+ require 'webrick'
3
+
4
+ require 'fluent/plugin/input'
5
+ require 'fluent/plugin/parser'
6
+
7
+ require 'fluent/plugin/gcloud_pubsub/client'
8
+
9
+ module Fluent::Plugin
10
+ class GcloudPubSubInput < Input
11
+ Fluent::Plugin.register_input('gcloud_pubsub', self)
12
+
13
+ helpers :compat_parameters, :parser, :thread
14
+
15
+ DEFAULT_PARSER_TYPE = 'json'
16
+
17
+ class FailedParseError < StandardError
18
+ end
19
+
20
+ desc 'Set tag of messages.'
21
+ config_param :tag, :string
22
+ desc 'Set key to be used as tag.'
23
+ config_param :tag_key, :string, default: nil
24
+ desc 'Set your GCP project.'
25
+ config_param :project, :string, default: nil
26
+ desc 'Set your credential file path.'
27
+ config_param :key, :string, default: nil
28
+ desc 'Set topic name to pull.'
29
+ config_param :topic, :string
30
+ desc 'Set subscription name to pull.'
31
+ config_param :subscription, :string
32
+ desc 'Pulling messages by intervals of specified seconds.'
33
+ config_param :pull_interval, :float, default: 5.0
34
+ desc 'Max messages pulling at once.'
35
+ config_param :max_messages, :integer, default: 100
36
+ desc 'Setting `true`, keepalive connection to wait for new messages.'
37
+ config_param :return_immediately, :bool, default: true
38
+ desc 'Set number of threads to pull messages.'
39
+ config_param :pull_threads, :integer, default: 1
40
+ desc 'Specify the key of the attribute to be acquired as a record'
41
+ config_param :attribute_keys, :array, default: []
42
+ desc 'Set error type when parsing messages fails.'
43
+ config_param :parse_error_action, :enum, default: :exception, list: [:exception, :warning]
44
+ # for HTTP RPC
45
+ desc 'If `true` is specified, HTTP RPC to stop or start pulling message is enabled.'
46
+ config_param :enable_rpc, :bool, default: false
47
+ desc 'Bind IP address for HTTP RPC.'
48
+ config_param :rpc_bind, :string, default: '0.0.0.0'
49
+ desc 'Port for HTTP RPC.'
50
+ config_param :rpc_port, :integer, default: 24680
51
+
52
+ config_section :parse do
53
+ config_set_default :@type, DEFAULT_PARSER_TYPE
54
+ end
55
+
56
+ class RPCServlet < WEBrick::HTTPServlet::AbstractServlet
57
+ class Error < StandardError; end
58
+
59
+ def initialize(server, plugin)
60
+ super
61
+ @plugin = plugin
62
+ end
63
+
64
+ def do_GET(req, res)
65
+ begin
66
+ code, header, body = process(req, res)
67
+ rescue
68
+ code, header, body = render_json(500, {
69
+ 'ok' => false,
70
+ 'message' => 'Internal Server Error',
71
+ 'error' => "#{$!}",
72
+ 'backtrace'=> $!.backtrace
73
+ })
74
+ end
75
+
76
+ res.status = code
77
+ header.each_pair {|k,v|
78
+ res[k] = v
79
+ }
80
+ res.body = body
81
+ end
82
+
83
+ def render_json(code, obj)
84
+ [code, {'Content-Type' => 'application/json'}, obj.to_json]
85
+ end
86
+
87
+ def process(req, res)
88
+ ret = {'ok' => true}
89
+ case req.path_info
90
+ when '/stop'
91
+ @plugin.stop_pull
92
+ when '/start'
93
+ @plugin.start_pull
94
+ when '/status'
95
+ ret['status'] = @plugin.status_of_pull
96
+ else
97
+ raise Error.new "Invalid path_info: #{req.path_info}"
98
+ end
99
+ render_json(200, ret)
100
+ end
101
+ end
102
+
103
+ def configure(conf)
104
+ compat_parameters_convert(conf, :parser)
105
+ super
106
+ @rpc_srv = nil
107
+ @rpc_thread = nil
108
+ @stop_pull = false
109
+
110
+ @extract_tag = if @tag_key.nil?
111
+ method(:static_tag)
112
+ else
113
+ method(:dynamic_tag)
114
+ end
115
+
116
+ @parser = parser_create
117
+ end
118
+
119
+ def start
120
+ super
121
+ start_rpc if @enable_rpc
122
+
123
+ @subscriber = Fluent::GcloudPubSub::Subscriber.new @project, @key, @topic, @subscription
124
+ log.debug "connected subscription:#{@subscription} in project #{@project}"
125
+
126
+ @emit_guard = Mutex.new
127
+ @stop_subscribing = false
128
+ @subscribe_threads = []
129
+ @pull_threads.times do |idx|
130
+ @subscribe_threads.push thread_create("in_gcloud_pubsub_subscribe_#{idx}".to_sym, &method(:subscribe))
131
+ end
132
+ end
133
+
134
+ def shutdown
135
+ if @rpc_srv
136
+ @rpc_srv.shutdown
137
+ @rpc_srv = nil
138
+ end
139
+ if @rpc_thread
140
+ @rpc_thread = nil
141
+ end
142
+ @stop_subscribing = true
143
+ @subscribe_threads.each(&:join)
144
+ super
145
+ end
146
+
147
+ def stop_pull
148
+ @stop_pull = true
149
+ log.info "stop pull from subscription:#{@subscription}"
150
+ end
151
+
152
+ def start_pull
153
+ @stop_pull = false
154
+ log.info "start pull from subscription:#{@subscription}"
155
+ end
156
+
157
+ def status_of_pull
158
+ @stop_pull ? 'stopped' : 'started'
159
+ end
160
+
161
+ private
162
+
163
+ def static_tag(record)
164
+ @tag
165
+ end
166
+
167
+ def dynamic_tag(record)
168
+ record.delete(@tag_key) || @tag
169
+ end
170
+
171
+ def start_rpc
172
+ log.info "listening http rpc server on http://#{@rpc_bind}:#{@rpc_port}/"
173
+ @rpc_srv = WEBrick::HTTPServer.new(
174
+ {
175
+ BindAddress: @rpc_bind,
176
+ Port: @rpc_port,
177
+ Logger: WEBrick::Log.new(STDERR, WEBrick::Log::FATAL),
178
+ AccessLog: []
179
+ }
180
+ )
181
+ @rpc_srv.mount('/api/in_gcloud_pubsub/pull/', RPCServlet, self)
182
+ @rpc_thread = thread_create(:in_gcloud_pubsub_rpc_thread){
183
+ @rpc_srv.start
184
+ }
185
+ end
186
+
187
+ def subscribe
188
+ until @stop_subscribing
189
+ _subscribe unless @stop_pull
190
+
191
+ if @return_immediately || @stop_pull
192
+ sleep @pull_interval
193
+ end
194
+ end
195
+ rescue => ex
196
+ log.error "unexpected error", error_message: ex.to_s, error_class: ex.class.to_s
197
+ log.error_backtrace ex.backtrace
198
+ end
199
+
200
+ def _subscribe
201
+ messages = @subscriber.pull @return_immediately, @max_messages
202
+ if messages.length == 0
203
+ log.debug "no messages are pulled"
204
+ return
205
+ end
206
+
207
+ process messages
208
+ @subscriber.acknowledge messages
209
+
210
+ log.debug "#{messages.length} message(s) processed"
211
+ rescue Fluent::GcloudPubSub::RetryableError => ex
212
+ log.warn "Retryable error occurs. Fluentd will retry.", error_message: ex.to_s, error_class: ex.class.to_s
213
+ rescue => ex
214
+ log.error "unexpected error", error_message: ex.to_s, error_class: ex.class.to_s
215
+ log.error_backtrace ex.backtrace
216
+ end
217
+
218
+ def process(messages)
219
+ event_streams = Hash.new do |hsh, key|
220
+ hsh[key] = Fluent::MultiEventStream.new
221
+ end
222
+
223
+ messages.each do |m|
224
+ line = m.message.data.chomp
225
+ attributes = m.attributes
226
+ @parser.parse(line) do |time, record|
227
+ if time && record
228
+ @attribute_keys.each do |key|
229
+ record[key] = attributes[key]
230
+ end
231
+
232
+ event_streams[@extract_tag.call(record)].add(time, record)
233
+ else
234
+ case @parse_error_action
235
+ when :exception
236
+ raise FailedParseError.new "pattern not match: #{line}"
237
+ else
238
+ log.warn 'pattern not match', record: line
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ event_streams.each do |tag, es|
245
+ # There are some output plugins not to supposed to be called with multi-threading.
246
+ # Maybe remove in the future.
247
+ @emit_guard.synchronize do
248
+ router.emit_stream(tag, es)
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,110 @@
1
+ require 'fluent/plugin/output'
2
+ require 'fluent/plugin/gcloud_pubsub/client'
3
+ require 'fluent/plugin_helper/inject'
4
+
5
+ module Fluent::Plugin
6
+ class GcloudPubSubOutput < Output
7
+ include Fluent::PluginHelper::Inject
8
+
9
+ Fluent::Plugin.register_output('gcloud_pubsub', self)
10
+
11
+ helpers :compat_parameters, :formatter
12
+
13
+ DEFAULT_BUFFER_TYPE = "memory"
14
+ DEFAULT_FORMATTER_TYPE = "json"
15
+
16
+ desc 'Set your GCP project.'
17
+ config_param :project, :string, :default => nil
18
+ desc 'Set your credential file path.'
19
+ config_param :key, :string, :default => nil
20
+ desc 'Set topic name to publish.'
21
+ config_param :topic, :string
22
+ desc "If set to `true`, specified topic will be created when it doesn't exist."
23
+ config_param :autocreate_topic, :bool, :default => false
24
+ desc 'Publishing messages count per request to Cloud Pub/Sub.'
25
+ config_param :max_messages, :integer, :default => 1000
26
+ desc 'Publishing messages bytesize per request to Cloud Pub/Sub.'
27
+ config_param :max_total_size, :integer, :default => 9800000 # 9.8MB
28
+ desc 'Limit bytesize per message.'
29
+ config_param :max_message_size, :integer, :default => 4000000 # 4MB
30
+ desc 'Publishing the set field as an attribute'
31
+ config_param :attribute_keys, :array, :default => []
32
+
33
+ config_section :buffer do
34
+ config_set_default :@type, DEFAULT_BUFFER_TYPE
35
+ end
36
+
37
+ config_section :format do
38
+ config_set_default :@type, DEFAULT_FORMATTER_TYPE
39
+ end
40
+
41
+ def configure(conf)
42
+ compat_parameters_convert(conf, :buffer, :formatter)
43
+ super
44
+ placeholder_validate!(:topic, @topic)
45
+ @formatter = formatter_create
46
+ end
47
+
48
+ def start
49
+ super
50
+ @publisher = Fluent::GcloudPubSub::Publisher.new @project, @key, @autocreate_topic
51
+ end
52
+
53
+ def format(tag, time, record)
54
+ record = inject_values_to_record(tag, time, record)
55
+ attributes = {}
56
+ @attribute_keys.each do |key|
57
+ attributes[key] = record.delete(key)
58
+ end
59
+ [@formatter.format(tag, time, record), attributes].to_msgpack
60
+ end
61
+
62
+ def formatted_to_msgpack_binary?
63
+ true
64
+ end
65
+
66
+ def multi_workers_ready?
67
+ true
68
+ end
69
+
70
+ def write(chunk)
71
+ topic = extract_placeholders(@topic, chunk.metadata)
72
+
73
+ messages = []
74
+ size = 0
75
+
76
+ chunk.msgpack_each do |msg, attr|
77
+ msg = Fluent::GcloudPubSub::Message.new(msg, attr)
78
+ if msg.bytesize > @max_message_size
79
+ log.warn 'Drop a message because its size exceeds `max_message_size`', size: msg.bytesize
80
+ next
81
+ end
82
+ if messages.length + 1 > @max_messages || size + msg.bytesize > @max_total_size
83
+ publish(topic, messages)
84
+ messages = []
85
+ size = 0
86
+ end
87
+ messages << msg
88
+ size += msg.bytesize
89
+ end
90
+
91
+ if messages.length > 0
92
+ publish(topic, messages)
93
+ end
94
+ rescue Fluent::GcloudPubSub::RetryableError => ex
95
+ log.warn "Retryable error occurs. Fluentd will retry.", error_message: ex.to_s, error_class: ex.class.to_s
96
+ raise ex
97
+ rescue => ex
98
+ log.error "unexpected error", error_message: ex.to_s, error_class: ex.class.to_s
99
+ log.error_backtrace
100
+ raise ex
101
+ end
102
+
103
+ private
104
+
105
+ def publish(topic, messages)
106
+ log.debug "send message topic:#{topic} length:#{messages.length} size:#{messages.map(&:bytesize).inject(:+)}"
107
+ @publisher.publish(topic, messages)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,387 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ require_relative "../test_helper"
5
+ require "fluent/test/driver/input"
6
+
7
+ class GcloudPubSubInputTest < Test::Unit::TestCase
8
+ CONFIG = %[
9
+ tag test
10
+ project project-test
11
+ topic topic-test
12
+ subscription subscription-test
13
+ key key-test
14
+ ]
15
+
16
+ DEFAULT_HOST = '127.0.0.1'
17
+ DEFAULT_PORT = 24680
18
+
19
+ class DummyInvalidMsgData
20
+ def data
21
+ 'foo:bar'
22
+ end
23
+ end
24
+ class DummyInvalidMessage
25
+ def message
26
+ DummyInvalidMsgData.new
27
+ end
28
+ def attributes
29
+ {"attr_1" => "a", "attr_2" => "b"}
30
+ end
31
+ end
32
+
33
+ def create_driver(conf=CONFIG)
34
+ Fluent::Test::Driver::Input.new(Fluent::Plugin::GcloudPubSubInput).configure(conf)
35
+ end
36
+
37
+ def http_get(path)
38
+ http = Net::HTTP.new(DEFAULT_HOST, DEFAULT_PORT)
39
+ req = Net::HTTP::Get.new(path, {'Content-Type' => 'application/x-www-form-urlencoded'})
40
+ http.request(req)
41
+ end
42
+
43
+ setup do
44
+ Fluent::Test.setup
45
+ end
46
+
47
+ sub_test_case 'configure' do
48
+ test 'all params are configured' do
49
+ d = create_driver(%[
50
+ tag test
51
+ project project-test
52
+ topic topic-test
53
+ subscription subscription-test
54
+ key key-test
55
+ max_messages 1000
56
+ return_immediately true
57
+ pull_interval 2
58
+ pull_threads 3
59
+ attribute_keys attr-test
60
+ enable_rpc true
61
+ rpc_bind 127.0.0.1
62
+ rpc_port 24681
63
+ ])
64
+
65
+ assert_equal('test', d.instance.tag)
66
+ assert_equal('project-test', d.instance.project)
67
+ assert_equal('topic-test', d.instance.topic)
68
+ assert_equal('subscription-test', d.instance.subscription)
69
+ assert_equal('key-test', d.instance.key)
70
+ assert_equal(2.0, d.instance.pull_interval)
71
+ assert_equal(1000, d.instance.max_messages)
72
+ assert_equal(true, d.instance.return_immediately)
73
+ assert_equal(3, d.instance.pull_threads)
74
+ assert_equal(['attr-test'], d.instance.attribute_keys)
75
+ assert_equal(true, d.instance.enable_rpc)
76
+ assert_equal('127.0.0.1', d.instance.rpc_bind)
77
+ assert_equal(24681, d.instance.rpc_port)
78
+ end
79
+
80
+ test 'default values are configured' do
81
+ d = create_driver
82
+ assert_equal(5.0, d.instance.pull_interval)
83
+ assert_equal(100, d.instance.max_messages)
84
+ assert_equal(true, d.instance.return_immediately)
85
+ assert_equal(1, d.instance.pull_threads)
86
+ assert_equal([], d.instance.attribute_keys)
87
+ assert_equal(false, d.instance.enable_rpc)
88
+ assert_equal('0.0.0.0', d.instance.rpc_bind)
89
+ assert_equal(24680, d.instance.rpc_port)
90
+ end
91
+ end
92
+
93
+ sub_test_case 'start' do
94
+ setup do
95
+ @topic_mock = mock!
96
+ @pubsub_mock = mock!.topic('topic-test').at_least(1) { @topic_mock }
97
+ stub(Google::Cloud::Pubsub).new { @pubsub_mock }
98
+ end
99
+
100
+ test '40x error occurred on connecting to Pub/Sub' do
101
+ @topic_mock.subscription('subscription-test').once do
102
+ raise Google::Cloud::NotFoundError.new('TEST')
103
+ end
104
+
105
+ d = create_driver
106
+ assert_raise Google::Cloud::NotFoundError do
107
+ d.run {}
108
+ end
109
+ end
110
+
111
+ test '50x error occurred on connecting to Pub/Sub' do
112
+ @topic_mock.subscription('subscription-test').once do
113
+ raise Google::Cloud::UnavailableError.new('TEST')
114
+ end
115
+
116
+ d = create_driver
117
+ assert_raise Google::Cloud::UnavailableError do
118
+ d.run {}
119
+ end
120
+ end
121
+
122
+ test 'subscription is nil' do
123
+ @topic_mock.subscription('subscription-test').once { nil }
124
+
125
+ d = create_driver
126
+ assert_raise Fluent::GcloudPubSub::Error do
127
+ d.run {}
128
+ end
129
+ end
130
+ end
131
+
132
+ sub_test_case 'emit' do
133
+ class DummyMsgData
134
+ def data
135
+ '{"foo": "bar"}'
136
+ end
137
+ end
138
+ class DummyMessage
139
+ def message
140
+ DummyMsgData.new
141
+ end
142
+ def attributes
143
+ {"attr_1" => "a", "attr_2" => "b"}
144
+ end
145
+ end
146
+
147
+ class DummyMsgDataWithTagKey
148
+ def initialize(tag)
149
+ @tag = tag
150
+ end
151
+ def data
152
+ '{"foo": "bar", "test_tag_key": "' + @tag + '"}'
153
+ end
154
+ end
155
+ class DummyMessageWithTagKey
156
+ def initialize(tag)
157
+ @tag = tag
158
+ end
159
+ def message
160
+ DummyMsgDataWithTagKey.new @tag
161
+ end
162
+ def attributes
163
+ {"attr_1" => "a", "attr_2" => "b"}
164
+ end
165
+ end
166
+
167
+ setup do
168
+ @subscriber = mock!
169
+ @topic_mock = mock!.subscription('subscription-test') { @subscriber }
170
+ @pubsub_mock = mock!.topic('topic-test') { @topic_mock }
171
+ stub(Google::Cloud::Pubsub).new { @pubsub_mock }
172
+ end
173
+
174
+ test 'empty' do
175
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { [] }
176
+ @subscriber.acknowledge.times(0)
177
+
178
+ d = create_driver
179
+ d.run(expect_emits: 1, timeout: 3)
180
+
181
+ assert_true d.events.empty?
182
+ end
183
+
184
+ test 'simple' do
185
+ messages = Array.new(1, DummyMessage.new)
186
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { messages }
187
+ @subscriber.acknowledge(messages).at_least(1)
188
+
189
+ d = create_driver
190
+ d.run(expect_emits: 1, timeout: 3)
191
+ emits = d.events
192
+
193
+ assert(1 <= emits.length)
194
+ emits.each do |tag, time, record|
195
+ assert_equal("test", tag)
196
+ assert_equal({"foo" => "bar"}, record)
197
+ end
198
+ end
199
+
200
+ test 'multithread' do
201
+ messages = Array.new(1, DummyMessage.new)
202
+ @subscriber.pull(immediate: true, max: 100).at_least(2) { messages }
203
+ @subscriber.acknowledge(messages).at_least(2)
204
+
205
+ d = create_driver("#{CONFIG}\npull_threads 2")
206
+ d.run(expect_emits: 2, timeout: 1)
207
+ emits = d.events
208
+
209
+ assert(2 <= emits.length)
210
+ emits.each do |tag, time, record|
211
+ assert_equal("test", tag)
212
+ assert_equal({"foo" => "bar"}, record)
213
+ end
214
+ end
215
+
216
+ test 'with tag_key' do
217
+ messages = [
218
+ DummyMessageWithTagKey.new('tag1'),
219
+ DummyMessageWithTagKey.new('tag2'),
220
+ DummyMessage.new
221
+ ]
222
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { messages }
223
+ @subscriber.acknowledge(messages).at_least(1)
224
+
225
+ d = create_driver("#{CONFIG}\ntag_key test_tag_key")
226
+ d.run(expect_emits: 1, timeout: 3)
227
+ emits = d.events
228
+
229
+ assert(3 <= emits.length)
230
+ # test tag
231
+ assert_equal("tag1", emits[0][0])
232
+ assert_equal("tag2", emits[1][0])
233
+ assert_equal("test", emits[2][0])
234
+ # test record
235
+ emits.each do |tag, time, record|
236
+ assert_equal({"foo" => "bar"}, record)
237
+ end
238
+ end
239
+
240
+ test 'invalid messages with parse_error_action exception ' do
241
+ messages = Array.new(1, DummyInvalidMessage.new)
242
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { messages }
243
+ @subscriber.acknowledge.times(0)
244
+
245
+ d = create_driver
246
+ d.run(expect_emits: 1, timeout: 3)
247
+ assert_true d.events.empty?
248
+ end
249
+
250
+ test 'with attributes' do
251
+ messages = Array.new(1, DummyMessage.new)
252
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { messages }
253
+ @subscriber.acknowledge(messages).at_least(1)
254
+
255
+ d = create_driver("#{CONFIG}\nattribute_keys attr_1")
256
+ d.run(expect_emits: 1, timeout: 3)
257
+ emits = d.events
258
+
259
+ assert(1 <= emits.length)
260
+ emits.each do |tag, time, record|
261
+ assert_equal("test", tag)
262
+ assert_equal({"foo" => "bar", "attr_1" => "a"}, record)
263
+ end
264
+ end
265
+
266
+ test 'invalid messages with parse_error_action warning' do
267
+ messages = Array.new(1, DummyInvalidMessage.new)
268
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { messages }
269
+ @subscriber.acknowledge(messages).at_least(1)
270
+
271
+ d = create_driver("#{CONFIG}\nparse_error_action warning")
272
+ d.run(expect_emits: 1, timeout: 3)
273
+ assert_true d.events.empty?
274
+ end
275
+
276
+ test 'retry if raised error' do
277
+ class UnknownError < StandardError
278
+ end
279
+ @subscriber.pull(immediate: true, max: 100).at_least(2) { raise UnknownError.new('test') }
280
+ @subscriber.acknowledge.times(0)
281
+
282
+ d = create_driver(CONFIG + 'pull_interval 0.5')
283
+ d.run(expect_emits: 1, timeout: 0.8)
284
+
285
+ assert_equal(0.5, d.instance.pull_interval)
286
+ assert_true d.events.empty?
287
+ end
288
+
289
+ test 'retry if raised RetryableError on pull' do
290
+ @subscriber.pull(immediate: true, max: 100).at_least(2) { raise Google::Cloud::UnavailableError.new('TEST') }
291
+ @subscriber.acknowledge.times(0)
292
+
293
+ d = create_driver("#{CONFIG}\npull_interval 0.5")
294
+ d.run(expect_emits: 1, timeout: 0.8)
295
+
296
+ assert_equal(0.5, d.instance.pull_interval)
297
+ assert_true d.events.empty?
298
+ end
299
+
300
+ test 'retry if raised RetryableError on acknowledge' do
301
+ messages = Array.new(1, DummyMessage.new)
302
+ @subscriber.pull(immediate: true, max: 100).at_least(2) { messages }
303
+ @subscriber.acknowledge(messages).at_least(2) { raise Google::Cloud::UnavailableError.new('TEST') }
304
+
305
+ d = create_driver("#{CONFIG}\npull_interval 0.5")
306
+ d.run(expect_emits: 2, timeout: 3)
307
+ emits = d.events
308
+
309
+ # not acknowledged, but already emitted to engine.
310
+ assert(2 <= emits.length)
311
+ emits.each do |tag, time, record|
312
+ assert_equal("test", tag)
313
+ assert_equal({"foo" => "bar"}, record)
314
+ end
315
+ end
316
+
317
+ test 'stop by http rpc' do
318
+ messages = Array.new(1, DummyMessage.new)
319
+ @subscriber.pull(immediate: true, max: 100).once { messages }
320
+ @subscriber.acknowledge(messages).once
321
+
322
+ d = create_driver("#{CONFIG}\npull_interval 1.0\nenable_rpc true")
323
+ assert_equal(false, d.instance.instance_variable_get(:@stop_pull))
324
+
325
+ d.run {
326
+ http_get('/api/in_gcloud_pubsub/pull/stop')
327
+ sleep 0.75
328
+ # d.run sleeps 0.5 sec
329
+ }
330
+ emits = d.events
331
+
332
+ assert_equal(1, emits.length)
333
+ assert_true d.instance.instance_variable_get(:@stop_pull)
334
+
335
+ emits.each do |tag, time, record|
336
+ assert_equal("test", tag)
337
+ assert_equal({"foo" => "bar"}, record)
338
+ end
339
+ end
340
+
341
+ test 'start by http rpc' do
342
+ messages = Array.new(1, DummyMessage.new)
343
+ @subscriber.pull(immediate: true, max: 100).at_least(1) { messages }
344
+ @subscriber.acknowledge(messages).at_least(1)
345
+
346
+ d = create_driver("#{CONFIG}\npull_interval 1.0\nenable_rpc true")
347
+ d.instance.stop_pull
348
+ assert_equal(true, d.instance.instance_variable_get(:@stop_pull))
349
+
350
+ d.run(expect_emits: 1, timeout: 3) {
351
+ http_get('/api/in_gcloud_pubsub/pull/start')
352
+ sleep 0.75
353
+ # d.run sleeps 0.5 sec
354
+ }
355
+ emits = d.events
356
+
357
+ assert_equal(true, emits.length > 0)
358
+ assert_false d.instance.instance_variable_get(:@stop_pull)
359
+
360
+ emits.each do |tag, time, record|
361
+ assert_equal("test", tag)
362
+ assert_equal({"foo" => "bar"}, record)
363
+ end
364
+ end
365
+
366
+ test 'get status by http rpc when started' do
367
+ d = create_driver("#{CONFIG}\npull_interval 1.0\nenable_rpc true")
368
+ assert_false d.instance.instance_variable_get(:@stop_pull)
369
+
370
+ d.run {
371
+ res = http_get('/api/in_gcloud_pubsub/pull/status')
372
+ assert_equal({"ok" => true, "status" => "started"}, JSON.parse(res.body))
373
+ }
374
+ end
375
+
376
+ test 'get status by http rpc when stopped' do
377
+ d = create_driver("#{CONFIG}\npull_interval 1.0\nenable_rpc true")
378
+ d.instance.stop_pull
379
+ assert_true d.instance.instance_variable_get(:@stop_pull)
380
+
381
+ d.run {
382
+ res = http_get('/api/in_gcloud_pubsub/pull/status')
383
+ assert_equal({"ok" => true, "status" => "stopped"}, JSON.parse(res.body))
384
+ }
385
+ end
386
+ end
387
+ end