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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +2 -0
  5. data/README.md +401 -0
  6. data/Rakefile +111 -0
  7. data/ext/.gitignore +3 -0
  8. data/ext/pom.xml +63 -0
  9. data/ext/pom.xml.template +65 -0
  10. data/ext/src/main/java/com/kickstarter/jruby/Telekinesis.java +103 -0
  11. data/lib/telekinesis/aws/client_adapter.rb +61 -0
  12. data/lib/telekinesis/aws/java_client_adapter.rb +72 -0
  13. data/lib/telekinesis/aws/ruby_client_adapter.rb +40 -0
  14. data/lib/telekinesis/aws.rb +9 -0
  15. data/lib/telekinesis/consumer/base_processor.rb +12 -0
  16. data/lib/telekinesis/consumer/block.rb +22 -0
  17. data/lib/telekinesis/consumer/distributed_consumer.rb +114 -0
  18. data/lib/telekinesis/consumer.rb +3 -0
  19. data/lib/telekinesis/java_util.rb +46 -0
  20. data/lib/telekinesis/logging/java_logging.rb +18 -0
  21. data/lib/telekinesis/logging/ruby_logger_handler.rb +54 -0
  22. data/lib/telekinesis/producer/async_producer.rb +157 -0
  23. data/lib/telekinesis/producer/async_producer_worker.rb +110 -0
  24. data/lib/telekinesis/producer/noop_failure_handler.rb +12 -0
  25. data/lib/telekinesis/producer/sync_producer.rb +52 -0
  26. data/lib/telekinesis/producer/warn_failure_handler.rb +25 -0
  27. data/lib/telekinesis/producer.rb +4 -0
  28. data/lib/telekinesis/telekinesis-2.0.0.jar +0 -0
  29. data/lib/telekinesis/version.rb +3 -0
  30. data/lib/telekinesis.rb +14 -0
  31. data/telekinesis.gemspec +21 -0
  32. data/test/aws/test_client_adapter.rb +29 -0
  33. data/test/aws/test_java_client_adapter.rb +72 -0
  34. data/test/producer/test_async_producer.rb +158 -0
  35. data/test/producer/test_async_producer_worker.rb +390 -0
  36. data/test/producer/test_helper.rb +1 -0
  37. data/test/producer/test_sync_producer.rb +144 -0
  38. data/test/test_helper.rb +6 -0
  39. 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
@@ -0,0 +1,4 @@
1
+ require "telekinesis/producer/sync_producer"
2
+ require "telekinesis/producer/noop_failure_handler"
3
+ require "telekinesis/producer/warn_failure_handler"
4
+ require "telekinesis/producer/async_producer"
@@ -0,0 +1,3 @@
1
+ module Telekinesis
2
+ VERSION = '2.0.0'
3
+ end
@@ -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"
@@ -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