telekinesis 2.0.0-java

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