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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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