telekinesis 2.0.0-java
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 +5 -0
- data/.ruby-version +1 -0
- data/Gemfile +2 -0
- data/README.md +401 -0
- data/Rakefile +111 -0
- data/ext/.gitignore +3 -0
- data/ext/pom.xml +63 -0
- data/ext/pom.xml.template +65 -0
- data/ext/src/main/java/com/kickstarter/jruby/Telekinesis.java +103 -0
- data/lib/telekinesis/aws/client_adapter.rb +61 -0
- data/lib/telekinesis/aws/java_client_adapter.rb +72 -0
- data/lib/telekinesis/aws/ruby_client_adapter.rb +40 -0
- data/lib/telekinesis/aws.rb +9 -0
- data/lib/telekinesis/consumer/base_processor.rb +12 -0
- data/lib/telekinesis/consumer/block.rb +22 -0
- data/lib/telekinesis/consumer/distributed_consumer.rb +114 -0
- data/lib/telekinesis/consumer.rb +3 -0
- data/lib/telekinesis/java_util.rb +46 -0
- data/lib/telekinesis/logging/java_logging.rb +18 -0
- data/lib/telekinesis/logging/ruby_logger_handler.rb +54 -0
- data/lib/telekinesis/producer/async_producer.rb +157 -0
- data/lib/telekinesis/producer/async_producer_worker.rb +110 -0
- data/lib/telekinesis/producer/noop_failure_handler.rb +12 -0
- data/lib/telekinesis/producer/sync_producer.rb +52 -0
- data/lib/telekinesis/producer/warn_failure_handler.rb +25 -0
- data/lib/telekinesis/producer.rb +4 -0
- data/lib/telekinesis/telekinesis-2.0.0.jar +0 -0
- data/lib/telekinesis/version.rb +3 -0
- data/lib/telekinesis.rb +14 -0
- data/telekinesis.gemspec +21 -0
- data/test/aws/test_client_adapter.rb +29 -0
- data/test/aws/test_java_client_adapter.rb +72 -0
- data/test/producer/test_async_producer.rb +158 -0
- data/test/producer/test_async_producer_worker.rb +390 -0
- data/test/producer/test_helper.rb +1 -0
- data/test/producer/test_sync_producer.rb +144 -0
- data/test/test_helper.rb +6 -0
- metadata +149 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
require "telekinesis/producer/async_producer_worker"
|
2
|
+
|
3
|
+
module Telekinesis
|
4
|
+
module Producer
|
5
|
+
java_import java.util.concurrent.TimeUnit
|
6
|
+
java_import java.util.concurrent.Executors
|
7
|
+
java_import java.util.concurrent.ArrayBlockingQueue
|
8
|
+
java_import com.google.common.util.concurrent.ThreadFactoryBuilder
|
9
|
+
|
10
|
+
# An asynchronous producer that buffers events into a queue and uses a
|
11
|
+
# background thread to send them to Kinesis. Only available on JRuby.
|
12
|
+
#
|
13
|
+
# This class is thread-safe.
|
14
|
+
class AsyncProducer
|
15
|
+
# For convenience
|
16
|
+
MAX_PUT_RECORDS_SIZE = Telekinesis::Aws::KINESIS_MAX_PUT_RECORDS_SIZE
|
17
|
+
|
18
|
+
attr_reader :stream, :client, :failure_handler
|
19
|
+
|
20
|
+
# Create a new producer.
|
21
|
+
#
|
22
|
+
# AWS credentials may be specified by using the `:credentials` option and
|
23
|
+
# passing a hash containing your `:access_key_id` and `:secret_access_key`.
|
24
|
+
# If unspecified, credentials will be fetched from the environment, an
|
25
|
+
# ~/.aws/credentials file, or the current instance metadata.
|
26
|
+
#
|
27
|
+
# The producer's `:worker_count`, internal `:queue_size`, the `:send_size`
|
28
|
+
# of batches to Kinesis and how often workers send data to Kinesis, even
|
29
|
+
# if their batches aren't full (`:send_every_ms`) can be configured as
|
30
|
+
# well. They all have reasonable defaults.
|
31
|
+
#
|
32
|
+
# When requests to Kinesis fail, the configured `:failure_handler` will
|
33
|
+
# be called. If you don't specify a failure handler, a NoopFailureHandler
|
34
|
+
# is used.
|
35
|
+
def self.create(options = {})
|
36
|
+
stream = options[:stream]
|
37
|
+
client = Telekinesis::Aws::Client.build(options.fetch(:credentials, {}))
|
38
|
+
failure_handler = options.fetch(:failure_handler, NoopFailureHandler.new)
|
39
|
+
new(stream, client, failure_handler, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Construct a new producer. Intended for internal use only - prefer
|
43
|
+
# #create unless it's strictly necessary.
|
44
|
+
def initialize(stream, client, failure_handler, options = {})
|
45
|
+
@stream = stream or raise ArgumentError, "stream may not be nil"
|
46
|
+
@client = client or raise ArgumentError, "client may not be nil"
|
47
|
+
@failure_handler = failure_handler or raise ArgumentError, "failure_handler may not be nil"
|
48
|
+
@shutdown = false
|
49
|
+
|
50
|
+
queue_size = options.fetch(:queue_size, 1000)
|
51
|
+
send_every = options.fetch(:send_every_ms, 1000)
|
52
|
+
worker_count = options.fetch(:worker_count, 1)
|
53
|
+
raise ArgumentError(":worker_count must be > 0") unless worker_count > 0
|
54
|
+
send_size = options.fetch(:send_size, MAX_PUT_RECORDS_SIZE)
|
55
|
+
raise ArgumentError(":send_size too large") if send_size > MAX_PUT_RECORDS_SIZE
|
56
|
+
retries = options.fetch(:retries, 5)
|
57
|
+
raise ArgumentError(":retries must be >= 0") unless retries >= 0
|
58
|
+
retry_interval = options.fetch(:retry_interval, 1.0)
|
59
|
+
raise ArgumentError(":retry_interval must be > 0") unless retry_interval > 0
|
60
|
+
|
61
|
+
# NOTE: For testing.
|
62
|
+
@queue = options[:queue] || ArrayBlockingQueue.new(queue_size)
|
63
|
+
|
64
|
+
@lock = Telekinesis::JavaUtil::ReadWriteLock.new
|
65
|
+
@worker_pool = build_executor(worker_count)
|
66
|
+
@workers = worker_count.times.map do
|
67
|
+
AsyncProducerWorker.new(self, @queue, send_size, send_every, retries, retry_interval)
|
68
|
+
end
|
69
|
+
|
70
|
+
# NOTE: Start by default. For testing.
|
71
|
+
start unless options.fetch(:manual_start, false)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Put a single key, value pair to Kinesis. Both key and value must be
|
75
|
+
# strings.
|
76
|
+
#
|
77
|
+
# This call returns immediately and returns true iff the producer is still
|
78
|
+
# accepting data. Data is put to Kinesis in the background.
|
79
|
+
def put(key, data)
|
80
|
+
put_all(key => data)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Put all of the given key, value pairs to Kinesis. Both key and value
|
84
|
+
# must be Strings.
|
85
|
+
#
|
86
|
+
# This call returns immediately and returns true iff the producer is still
|
87
|
+
# accepting data. Data is put to Kinesis in the background.
|
88
|
+
def put_all(items)
|
89
|
+
# NOTE: The lock ensures that no new data can be added to the queue after
|
90
|
+
# the shutdown flag has been set. See the note in shutdown for details.
|
91
|
+
@lock.read_lock do
|
92
|
+
if @shutdown
|
93
|
+
false
|
94
|
+
else
|
95
|
+
items.each do |key, data|
|
96
|
+
@queue.put([key, data])
|
97
|
+
end
|
98
|
+
true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Shut down this producer. After the call completes, the producer will not
|
104
|
+
# accept any more data, but will finish processing any data it has
|
105
|
+
# buffered internally.
|
106
|
+
#
|
107
|
+
# If block = true is passed, this call will block and wait for the producer
|
108
|
+
# to shut down before returning. This wait times out after duration has
|
109
|
+
# passed.
|
110
|
+
def shutdown(block = false, duration = 2, unit = TimeUnit::SECONDS)
|
111
|
+
# NOTE: Since a write_lock is exclusive, this prevents any data from being
|
112
|
+
# added to the queue while the SHUTDOWN tokens are being inserted. Without
|
113
|
+
# the lock, data can end up in the queue behind all of the shutdown tokens
|
114
|
+
# and be lost. This happens if the shutdown flag is be flipped by a thread
|
115
|
+
# calling shutdown after another thread has checked the "if @shutdown"
|
116
|
+
# condition in put but before it's called queue.put.
|
117
|
+
@lock.write_lock do
|
118
|
+
@shutdown = true
|
119
|
+
@workers.size.times do
|
120
|
+
@queue.put(AsyncProducerWorker::SHUTDOWN)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Don't interrupt workers by calling shutdown_now.
|
125
|
+
@worker_pool.shutdown
|
126
|
+
await(duration, unit) if block
|
127
|
+
end
|
128
|
+
|
129
|
+
# Wait for this producer to shutdown.
|
130
|
+
def await(duration, unit = TimeUnit::SECONDS)
|
131
|
+
@worker_pool.await_termination(duration, unit)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Return the number of events currently buffered by this producer. This
|
135
|
+
# doesn't include any events buffered in workers that are currently on
|
136
|
+
# their way to Kinesis.
|
137
|
+
def queue_size
|
138
|
+
@queue.size
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
def start
|
144
|
+
@workers.each do |w|
|
145
|
+
@worker_pool.java_send(:submit, [java.lang.Runnable.java_class], w)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def build_executor(worker_count)
|
150
|
+
Executors.new_fixed_thread_pool(
|
151
|
+
worker_count,
|
152
|
+
ThreadFactoryBuilder.new.set_name_format("#{stream}-producer-worker-%d").build
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Telekinesis
|
2
|
+
module Producer
|
3
|
+
java_import java.nio.ByteBuffer
|
4
|
+
java_import java.util.concurrent.TimeUnit
|
5
|
+
java_import com.amazonaws.services.kinesis.model.PutRecordsRequest
|
6
|
+
java_import com.amazonaws.services.kinesis.model.PutRecordsRequestEntry
|
7
|
+
|
8
|
+
class AsyncProducerWorker
|
9
|
+
SHUTDOWN = :shutdown
|
10
|
+
|
11
|
+
def initialize(producer, queue, send_size, send_every, retries, retry_interval)
|
12
|
+
@producer = producer
|
13
|
+
@queue = queue
|
14
|
+
@send_size = send_size
|
15
|
+
@send_every = send_every
|
16
|
+
@retries = retries
|
17
|
+
@retry_interval = retry_interval
|
18
|
+
|
19
|
+
@stream = producer.stream # for convenience
|
20
|
+
@client = producer.client # for convenience
|
21
|
+
@failure_handler = producer.failure_handler # for convenience
|
22
|
+
|
23
|
+
@buffer = []
|
24
|
+
@last_poll_at = current_time_millis
|
25
|
+
@shutdown = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
loop do
|
30
|
+
next_wait = [0, (@last_poll_at + @send_every) - current_time_millis].max
|
31
|
+
next_item = @queue.poll(next_wait, TimeUnit::MILLISECONDS)
|
32
|
+
|
33
|
+
if next_item == SHUTDOWN
|
34
|
+
next_item, @shutdown = nil, true
|
35
|
+
end
|
36
|
+
|
37
|
+
unless next_item.nil?
|
38
|
+
buffer(next_item)
|
39
|
+
end
|
40
|
+
|
41
|
+
if buffer_full || (next_item.nil? && buffer_has_records)
|
42
|
+
put_records(get_and_reset_buffer, @retries, @retry_interval)
|
43
|
+
end
|
44
|
+
|
45
|
+
@last_poll_at = current_time_millis
|
46
|
+
break if @shutdown
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
# TODO: is there a way to encourage people to set up an uncaught exception
|
50
|
+
# hanlder and/or disable this?
|
51
|
+
bt = e.backtrace ? e.backtrace.map{|l| "! #{l}"}.join("\n") : ""
|
52
|
+
warn "Producer background thread died!"
|
53
|
+
warn "#{e.class}: #{e.message}\n#{bt}"
|
54
|
+
raise e
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def current_time_millis
|
60
|
+
(Time.now.to_f * 1000).to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
def buffer(item)
|
64
|
+
@buffer << item
|
65
|
+
end
|
66
|
+
|
67
|
+
def buffer_full
|
68
|
+
@buffer.size == @send_size
|
69
|
+
end
|
70
|
+
|
71
|
+
def buffer_has_records
|
72
|
+
!@buffer.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_and_reset_buffer
|
76
|
+
ret, @buffer = @buffer, []
|
77
|
+
ret
|
78
|
+
end
|
79
|
+
|
80
|
+
def put_records(items, retries, retry_interval)
|
81
|
+
begin
|
82
|
+
failed = []
|
83
|
+
while retries > 0
|
84
|
+
retryable, unretryable = @client.put_records(@stream, items).partition do |_, _, code, _|
|
85
|
+
code == 'InternalFailure' || code == 'ProvisionedThroughputExceededException'
|
86
|
+
end
|
87
|
+
failed.concat(unretryable)
|
88
|
+
|
89
|
+
if retryable.empty?
|
90
|
+
break
|
91
|
+
else
|
92
|
+
items = retryable.map{|k, v, _, _| [k, v]}
|
93
|
+
retries -= 1
|
94
|
+
end
|
95
|
+
end
|
96
|
+
failed.concat(retryable) unless retryable.empty?
|
97
|
+
@failure_handler.on_record_failure(failed) unless failed.empty?
|
98
|
+
rescue Telekinesis::Aws::KinesisError => e
|
99
|
+
if e.cause && e.cause.is_retryable && (retries -= 1) > 0
|
100
|
+
sleep retry_interval
|
101
|
+
@failure_handler.on_kinesis_retry(e, items)
|
102
|
+
retry
|
103
|
+
else
|
104
|
+
@failure_handler.on_kinesis_failure(e, items)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Telekinesis
|
2
|
+
module Producer
|
3
|
+
# A failure handler that does nothing.
|
4
|
+
#
|
5
|
+
# Nothing!
|
6
|
+
class NoopFailureHandler
|
7
|
+
def on_record_failure(item_error_tuples); end
|
8
|
+
def on_kinesis_retry(error, items); end
|
9
|
+
def on_kinesis_failure(error, items); end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Telekinesis
|
2
|
+
module Producer
|
3
|
+
# A synchronous Kinesis producer.
|
4
|
+
#
|
5
|
+
# This class is thread safe if and only if the underlying
|
6
|
+
# Telekines::Aws::Client is threadsafe. In practice, this means this client
|
7
|
+
# is threadsafe on JRuby and not thread safe elsewhere.
|
8
|
+
class SyncProducer
|
9
|
+
attr_reader :stream, :client
|
10
|
+
|
11
|
+
# Create a new Producer.
|
12
|
+
#
|
13
|
+
# AWS credentials may be specified by using the `:credentials` option and
|
14
|
+
# passing a hash containing your `:access_key_id` and `:secret_access_key`.
|
15
|
+
# If unspecified, credentials will be fetched from the environment, an
|
16
|
+
# ~/.aws/credentials file, or the current instance metadata.
|
17
|
+
#
|
18
|
+
# `:send_size` may also be used to configure the maximum batch size used
|
19
|
+
# in `put_all`. See `put_all` for more info.
|
20
|
+
def self.create(options = {})
|
21
|
+
stream = options[:stream]
|
22
|
+
client = Telekinesis::Aws::Client.build(options.fetch(:credentials, {}))
|
23
|
+
new(stream, client, failure_handler, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(stream, client, opts = {})
|
27
|
+
@stream = stream or raise ArgumentError, "stream may not be nil"
|
28
|
+
@client = client or raise ArgumentError, "client may not be nil"
|
29
|
+
@send_size = opts.fetch(:send_size, Telekinesis::Aws::KINESIS_MAX_PUT_RECORDS_SIZE)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Put an individual k, v pair to Kinesis immediately. Both k and v must
|
33
|
+
# be strings.
|
34
|
+
#
|
35
|
+
# Returns once the call to Kinesis is complete.
|
36
|
+
def put(key, data)
|
37
|
+
@client.put_record(@stream, key, data)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Put all of the [k, v] pairs to Kinesis in as few requests as possible.
|
41
|
+
# All of the ks and vs must be strings.
|
42
|
+
#
|
43
|
+
# Each request sends at most `:send_size` records. By default this is the
|
44
|
+
# Kinesis API limit of 500 records.
|
45
|
+
def put_all(items)
|
46
|
+
items.each_slice(@send_size).flat_map do |batch|
|
47
|
+
@client.put_records(@stream, batch)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Telekinesis
|
2
|
+
module Producer
|
3
|
+
# A simple FailureHandler that logs errors with `warn`. Available as an
|
4
|
+
# example and an easy default.
|
5
|
+
class WarnFailureHandler
|
6
|
+
def on_record_failure(item_err_pairs)
|
7
|
+
warn "Puts for #{item_err_pairs.size} records failed!"
|
8
|
+
end
|
9
|
+
|
10
|
+
# Do nothing on retry. Let it figure itself out.
|
11
|
+
def on_kinesis_retry(err, items); end
|
12
|
+
|
13
|
+
def on_kinesis_failure(err, items)
|
14
|
+
warn "PutRecords request with #{items.size} items failed!"
|
15
|
+
warn format_bt(err)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def format_bt(e)
|
21
|
+
e.backtrace ? e.backtrace.map{|l| "! #{l}"}.join("\n") : ""
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
Binary file
|
data/lib/telekinesis.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Telekinesis; end
|
2
|
+
|
3
|
+
unless RUBY_PLATFORM.match(/java/)
|
4
|
+
raise "Sorry! Telekinesis is only supported on JRuby"
|
5
|
+
end
|
6
|
+
|
7
|
+
require "telekinesis/version"
|
8
|
+
require "telekinesis/telekinesis-#{Telekinesis::VERSION}.jar"
|
9
|
+
require "telekinesis/java_util"
|
10
|
+
require "telekinesis/logging/java_logging"
|
11
|
+
require "telekinesis/aws"
|
12
|
+
|
13
|
+
require "telekinesis/producer"
|
14
|
+
require "telekinesis/consumer"
|
data/telekinesis.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "telekinesis/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "telekinesis"
|
6
|
+
spec.version = Telekinesis::VERSION
|
7
|
+
spec.author = "Ben Linsay"
|
8
|
+
spec.email = "ben@kickstarter.com"
|
9
|
+
spec.summary = "High level clients for Amazon Kinesis"
|
10
|
+
spec.homepage = "https://github.com/kickstarter/telekinesis"
|
11
|
+
|
12
|
+
spec.platform = "java"
|
13
|
+
spec.files = `git ls-files`.split($/) + Dir.glob("lib/telekinesis/*.jar")
|
14
|
+
spec.require_paths = ["lib"]
|
15
|
+
spec.add_dependency "aws-sdk"
|
16
|
+
|
17
|
+
spec.add_development_dependency "rake"
|
18
|
+
spec.add_development_dependency "nokogiri"
|
19
|
+
spec.add_development_dependency "minitest"
|
20
|
+
spec.add_development_dependency "shoulda-context"
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class ClientAdapterTest < Minitest::Test
|
4
|
+
StubResponse = Struct.new(:error_code, :error_message)
|
5
|
+
|
6
|
+
class EvenRecordsAreErrors < Telekinesis::Aws::ClientAdapter
|
7
|
+
def do_put_records(stream, items)
|
8
|
+
items.each_with_index.map do |_, idx|
|
9
|
+
err, message = idx.even? ? ["error-#{idx}", "message-#{idx}"] : [nil, nil]
|
10
|
+
StubResponse.new(err, message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "ClientAdapter" do
|
16
|
+
context "put_records" do
|
17
|
+
setup do
|
18
|
+
@client = EvenRecordsAreErrors.new(nil)
|
19
|
+
@items = 10.times.map{|i| ["key-#{i}", "value-#{i}"]}
|
20
|
+
@expected = 10.times.select{|i| i.even?}
|
21
|
+
.map{|i| ["key-#{i}", "value-#{i}", "error-#{i}", "message-#{i}"]}
|
22
|
+
end
|
23
|
+
|
24
|
+
should "zip error responses with records" do
|
25
|
+
assert(@expected, @client.put_records('stream', @items))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class JavaClientAdapterTest < Minitest::Test
|
4
|
+
java_import com.amazonaws.services.kinesis.model.PutRecordRequest
|
5
|
+
java_import com.amazonaws.services.kinesis.model.PutRecordsRequest
|
6
|
+
|
7
|
+
SomeStruct = Struct.new(:field)
|
8
|
+
StubResponse = Struct.new(:records)
|
9
|
+
|
10
|
+
class EchoClient
|
11
|
+
def put_record(*args)
|
12
|
+
args
|
13
|
+
end
|
14
|
+
|
15
|
+
def put_records(*args)
|
16
|
+
StubResponse.new(args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "JavaClientAdapter" do
|
21
|
+
setup do
|
22
|
+
@client = Telekinesis::Aws::JavaClientAdapter.new(EchoClient.new)
|
23
|
+
end
|
24
|
+
|
25
|
+
context "#put_record" do
|
26
|
+
setup do
|
27
|
+
# No exceptions, coerced to string. [args, expected]
|
28
|
+
@data = [
|
29
|
+
[['stream', 'key', 'value'], ['stream', 'key', 'value']],
|
30
|
+
[['stream', 123, 123], ['stream', '123', '123']],
|
31
|
+
[['stream', SomeStruct.new('key'), SomeStruct.new('value')], ['stream', '#<struct JavaClientAdapterTest::SomeStruct field="key">', '#<struct JavaClientAdapterTest::SomeStruct field="value">']],
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
should "generate aws.PutRecordsRequest" do
|
36
|
+
@data.each do |args, expected|
|
37
|
+
request, = @client.put_record(*args)
|
38
|
+
expected_stream, expected_key, expected_value = expected
|
39
|
+
|
40
|
+
assert_equal(expected_stream, request.stream_name)
|
41
|
+
assert_equal(expected_key, request.partition_key)
|
42
|
+
assert_equal(expected_value, String.from_java_bytes(request.data.array))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "#do_put_records" do
|
48
|
+
setup do
|
49
|
+
# No exceptions, coerced to string. [args, expected]
|
50
|
+
@data = [
|
51
|
+
[
|
52
|
+
['stream', [['key', 'value'], [123, 123], [SomeStruct.new('key'), SomeStruct.new('value')]]],
|
53
|
+
['stream', [['key', 'value'], ['123', '123'], ['#<struct JavaClientAdapterTest::SomeStruct field="key">', '#<struct JavaClientAdapterTest::SomeStruct field="value">']]]
|
54
|
+
],
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
should "generate aws.PutRecordsRequest" do
|
59
|
+
@data.each do |args, expected|
|
60
|
+
request, = @client.send(:do_put_records, *args)
|
61
|
+
expected_stream, expected_items = expected
|
62
|
+
|
63
|
+
assert_equal(expected_stream, request.stream_name)
|
64
|
+
expected_items.zip(request.records) do |(expected_key, expected_value), record|
|
65
|
+
assert_equal(expected_key, record.partition_key)
|
66
|
+
assert_equal(expected_value, String.from_java_bytes(record.data.array))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class AsyncProducerTest < Minitest::Test
|
4
|
+
java_import java.util.concurrent.TimeUnit
|
5
|
+
java_import java.util.concurrent.CountDownLatch
|
6
|
+
java_import java.util.concurrent.ArrayBlockingQueue
|
7
|
+
|
8
|
+
StubClient = Struct.new(:welp)
|
9
|
+
|
10
|
+
class LatchQueue
|
11
|
+
def initialize
|
12
|
+
@under = ArrayBlockingQueue.new(100)
|
13
|
+
@latch = CountDownLatch.new(1)
|
14
|
+
@putting = CountDownLatch.new(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
def count_down
|
18
|
+
@latch.count_down
|
19
|
+
end
|
20
|
+
|
21
|
+
def wait_for_put
|
22
|
+
@putting.await
|
23
|
+
end
|
24
|
+
|
25
|
+
def put(item)
|
26
|
+
@putting.count_down
|
27
|
+
@latch.await
|
28
|
+
@under.put(item)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_producer
|
33
|
+
opts = {
|
34
|
+
queue: @queue,
|
35
|
+
manual_start: true,
|
36
|
+
worker_count: @worker_count,
|
37
|
+
}
|
38
|
+
Telekinesis::Producer::AsyncProducer.new(
|
39
|
+
@stream,
|
40
|
+
StubClient.new,
|
41
|
+
Telekinesis::Producer::NoopFailureHandler.new,
|
42
|
+
opts
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
context "AsyncProducer" do
|
47
|
+
setup do
|
48
|
+
@stream = 'test' # ignored
|
49
|
+
@worker_count = 3 # arbitrary
|
50
|
+
end
|
51
|
+
|
52
|
+
context "put" do
|
53
|
+
setup do
|
54
|
+
@queue = ArrayBlockingQueue.new(100)
|
55
|
+
build_producer.put("hi", "there")
|
56
|
+
end
|
57
|
+
|
58
|
+
should "add the k,v pair to the queue" do
|
59
|
+
assert_equal([["hi", "there"]], @queue.to_a)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "put_all" do
|
64
|
+
setup do
|
65
|
+
@items = 10.times.map{|i| ["key-#{i}", "value-#{i}"]}
|
66
|
+
@queue = ArrayBlockingQueue.new(100)
|
67
|
+
build_producer.put_all(@items)
|
68
|
+
end
|
69
|
+
|
70
|
+
should "add all items to the queue" do
|
71
|
+
assert_equal(@items, @queue.to_a)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "after shutdown" do
|
76
|
+
setup do
|
77
|
+
@queue = ArrayBlockingQueue.new(100)
|
78
|
+
@producer = build_producer
|
79
|
+
@producer.shutdown
|
80
|
+
end
|
81
|
+
|
82
|
+
should "shutdown all workers" do
|
83
|
+
assert_equal([Telekinesis::Producer::AsyncProducerWorker::SHUTDOWN] * @worker_count, @queue.to_a)
|
84
|
+
end
|
85
|
+
|
86
|
+
should "not accept events while shut down" do
|
87
|
+
refute(@producer.put("key", "value"))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "with a put in progress" do
|
92
|
+
setup do
|
93
|
+
@queue = LatchQueue.new
|
94
|
+
@producer = build_producer
|
95
|
+
|
96
|
+
# Thread blocks waiting for the latch in LatchQueue. Don't do any other
|
97
|
+
# set up until this thread is in the critical section.
|
98
|
+
Thread.new do
|
99
|
+
@producer.put("k", "v")
|
100
|
+
end
|
101
|
+
@queue.wait_for_put
|
102
|
+
|
103
|
+
# Thread blocks waiting for the write_lock in AsyncProducer. Once it's
|
104
|
+
# unblocked it signals by counting down shutdown_latch.
|
105
|
+
@shutdown_latch = CountDownLatch.new(1)
|
106
|
+
Thread.new do
|
107
|
+
@producer.shutdown
|
108
|
+
@shutdown_latch.count_down
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
should "block on shutdown until the put is done" do
|
113
|
+
# Check that the latch hasn't been triggered yet. Return immediately
|
114
|
+
# from the check - don't bother waiting.
|
115
|
+
refute(@shutdown_latch.await(0, TimeUnit::MILLISECONDS))
|
116
|
+
@queue.count_down
|
117
|
+
# NOTE: The assert is here to fail the test if it times out. This could
|
118
|
+
# effectively just be an await with no duration.
|
119
|
+
assert(@shutdown_latch.await(2, TimeUnit::SECONDS))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "with a shutdown in progress" do
|
124
|
+
setup do
|
125
|
+
@queue = LatchQueue.new
|
126
|
+
@producer = build_producer
|
127
|
+
|
128
|
+
# Thread blocks waiting to insert :shutdown into the queue because of
|
129
|
+
# the latch in LatchQueue. Don't do any other test set up until this
|
130
|
+
# thread is in the critical section.
|
131
|
+
Thread.new do
|
132
|
+
@producer.shutdown
|
133
|
+
end
|
134
|
+
@queue.wait_for_put
|
135
|
+
|
136
|
+
# This thread blocks waiting for the lock in AsyncProducer. Once it's
|
137
|
+
# done the put continues and then it signals completion by counting
|
138
|
+
# down finished_put_latch.
|
139
|
+
@finished_put_latch = CountDownLatch.new(1)
|
140
|
+
Thread.new do
|
141
|
+
@put_result = @producer.put("k", "v")
|
142
|
+
@finished_put_latch.count_down
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
should "block on a put" do
|
147
|
+
# Thread is already waiting in the critical section. Just check that
|
148
|
+
# the call hasn't exited yet and return immediately.
|
149
|
+
refute(@finished_put_latch.await(0, TimeUnit::MILLISECONDS))
|
150
|
+
@queue.count_down
|
151
|
+
# NOTE: The assert is here to fail the test if it times out. This could
|
152
|
+
# effectively just be an await with no duration.
|
153
|
+
assert(@finished_put_latch.await(2, TimeUnit::SECONDS))
|
154
|
+
refute(@put_result, "Producer should reject a put after shutdown")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|