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