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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +134 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +178 -0
- data/Rakefile +12 -0
- data/fluent-plugin-gcloud-pubsub-custom.gemspec +24 -0
- data/lib/fluent/plugin/gcloud_pubsub/client.rb +80 -0
- data/lib/fluent/plugin/in_gcloud_pubsub.rb +253 -0
- data/lib/fluent/plugin/out_gcloud_pubsub.rb +110 -0
- data/test/plugin/test_in_gcloud_pubsub.rb +387 -0
- data/test/plugin/test_out_gcloud_pubsub.rb +226 -0
- data/test/test_helper.rb +31 -0
- metadata +148 -0
@@ -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
|