ruby-kafka 0.7.0 → 0.7.1.beta1
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 +4 -4
- data/.circleci/config.yml +3 -3
- data/.gitignore +1 -0
- data/CHANGELOG.md +4 -0
- data/lib/kafka.rb +32 -0
- data/lib/kafka/broker.rb +18 -0
- data/lib/kafka/client.rb +38 -4
- data/lib/kafka/cluster.rb +60 -37
- data/lib/kafka/consumer.rb +2 -2
- data/lib/kafka/fetch_operation.rb +18 -59
- data/lib/kafka/fetched_batch.rb +9 -9
- data/lib/kafka/fetched_batch_generator.rb +114 -0
- data/lib/kafka/fetched_offset_resolver.rb +48 -0
- data/lib/kafka/fetcher.rb +2 -2
- data/lib/kafka/produce_operation.rb +52 -14
- data/lib/kafka/producer.rb +82 -2
- data/lib/kafka/protocol.rb +68 -48
- data/lib/kafka/protocol/add_partitions_to_txn_request.rb +34 -0
- data/lib/kafka/protocol/add_partitions_to_txn_response.rb +47 -0
- data/lib/kafka/protocol/decoder.rb +3 -6
- data/lib/kafka/protocol/encoder.rb +6 -11
- data/lib/kafka/protocol/end_txn_request.rb +29 -0
- data/lib/kafka/protocol/end_txn_response.rb +19 -0
- data/lib/kafka/protocol/fetch_request.rb +3 -1
- data/lib/kafka/protocol/fetch_response.rb +37 -18
- data/lib/kafka/protocol/init_producer_id_request.rb +26 -0
- data/lib/kafka/protocol/init_producer_id_response.rb +27 -0
- data/lib/kafka/protocol/list_offset_request.rb +8 -2
- data/lib/kafka/protocol/list_offset_response.rb +11 -6
- data/lib/kafka/protocol/record.rb +9 -0
- data/lib/kafka/protocol/record_batch.rb +17 -1
- data/lib/kafka/ssl_context.rb +19 -5
- data/lib/kafka/transaction_manager.rb +261 -0
- data/lib/kafka/transaction_state_machine.rb +72 -0
- data/lib/kafka/version.rb +1 -1
- data/ruby-kafka.gemspec +1 -1
- metadata +20 -4
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
module Protocol
|
5
|
+
class InitProducerIDResponse
|
6
|
+
attr_reader :error_code, :producer_id, :producer_epoch
|
7
|
+
|
8
|
+
def initialize(error_code:, producer_id:, producer_epoch:)
|
9
|
+
@error_code = error_code
|
10
|
+
@producer_id = producer_id
|
11
|
+
@producer_epoch = producer_epoch
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.decode(decoder)
|
15
|
+
_throttle_time_ms = decoder.int32
|
16
|
+
error_code = decoder.int16
|
17
|
+
producer_id = decoder.int64
|
18
|
+
producer_epoch = decoder.int16
|
19
|
+
new(
|
20
|
+
error_code: error_code,
|
21
|
+
producer_id: producer_id,
|
22
|
+
producer_epoch: producer_epoch
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -8,12 +8,14 @@ module Kafka
|
|
8
8
|
#
|
9
9
|
# OffsetRequest => ReplicaId [TopicName [Partition Time MaxNumberOfOffsets]]
|
10
10
|
# ReplicaId => int32
|
11
|
+
# IsolationLevel => int8
|
11
12
|
# TopicName => string
|
12
13
|
# Partition => int32
|
13
14
|
# Time => int64
|
14
|
-
# MaxNumberOfOffsets => int32
|
15
15
|
#
|
16
16
|
class ListOffsetRequest
|
17
|
+
ISOLATION_READ_UNCOMMITTED = 0
|
18
|
+
ISOLATION_READ_COMMITTED = 1
|
17
19
|
|
18
20
|
# @param topics [Hash]
|
19
21
|
def initialize(topics:)
|
@@ -21,6 +23,10 @@ module Kafka
|
|
21
23
|
@topics = topics
|
22
24
|
end
|
23
25
|
|
26
|
+
def api_version
|
27
|
+
2
|
28
|
+
end
|
29
|
+
|
24
30
|
def api_key
|
25
31
|
LIST_OFFSET_API
|
26
32
|
end
|
@@ -31,6 +37,7 @@ module Kafka
|
|
31
37
|
|
32
38
|
def encode(encoder)
|
33
39
|
encoder.write_int32(@replica_id)
|
40
|
+
encoder.write_int8(ISOLATION_READ_COMMITTED)
|
34
41
|
|
35
42
|
encoder.write_array(@topics) do |topic, partitions|
|
36
43
|
encoder.write_string(topic)
|
@@ -38,7 +45,6 @@ module Kafka
|
|
38
45
|
encoder.write_array(partitions) do |partition|
|
39
46
|
encoder.write_int32(partition.fetch(:partition))
|
40
47
|
encoder.write_int64(partition.fetch(:time))
|
41
|
-
encoder.write_int32(partition.fetch(:max_offsets))
|
42
48
|
end
|
43
49
|
end
|
44
50
|
end
|
@@ -8,9 +8,11 @@ module Kafka
|
|
8
8
|
# ## API Specification
|
9
9
|
#
|
10
10
|
# OffsetResponse => [TopicName [PartitionOffsets]]
|
11
|
-
#
|
11
|
+
# ThrottleTimeMS => int32
|
12
|
+
# PartitionOffsets => Partition ErrorCode Timestamp Offset
|
12
13
|
# Partition => int32
|
13
14
|
# ErrorCode => int16
|
15
|
+
# Timestamp => int64
|
14
16
|
# Offset => int64
|
15
17
|
#
|
16
18
|
class ListOffsetResponse
|
@@ -24,12 +26,13 @@ module Kafka
|
|
24
26
|
end
|
25
27
|
|
26
28
|
class PartitionOffsetInfo
|
27
|
-
attr_reader :partition, :error_code, :
|
29
|
+
attr_reader :partition, :error_code, :timestamp, :offset
|
28
30
|
|
29
|
-
def initialize(partition:, error_code:,
|
31
|
+
def initialize(partition:, error_code:, timestamp:, offset:)
|
30
32
|
@partition = partition
|
31
33
|
@error_code = error_code
|
32
|
-
@
|
34
|
+
@timestamp = timestamp
|
35
|
+
@offset = offset
|
33
36
|
end
|
34
37
|
end
|
35
38
|
|
@@ -56,10 +59,11 @@ module Kafka
|
|
56
59
|
|
57
60
|
Protocol.handle_error(partition_info.error_code)
|
58
61
|
|
59
|
-
partition_info.
|
62
|
+
partition_info.offset
|
60
63
|
end
|
61
64
|
|
62
65
|
def self.decode(decoder)
|
66
|
+
_throttle_time_ms = decoder.int32
|
63
67
|
topics = decoder.array do
|
64
68
|
name = decoder.string
|
65
69
|
|
@@ -67,7 +71,8 @@ module Kafka
|
|
67
71
|
PartitionOffsetInfo.new(
|
68
72
|
partition: decoder.int32,
|
69
73
|
error_code: decoder.int16,
|
70
|
-
|
74
|
+
timestamp: decoder.int64,
|
75
|
+
offset: decoder.int64
|
71
76
|
)
|
72
77
|
end
|
73
78
|
|
@@ -10,6 +10,7 @@ module Kafka
|
|
10
10
|
headers: {},
|
11
11
|
attributes: 0,
|
12
12
|
offset_delta: 0,
|
13
|
+
offset: 0,
|
13
14
|
timestamp_delta: 0,
|
14
15
|
create_time: Time.now,
|
15
16
|
is_control_record: false
|
@@ -20,6 +21,7 @@ module Kafka
|
|
20
21
|
@attributes = attributes
|
21
22
|
|
22
23
|
@offset_delta = offset_delta
|
24
|
+
@offset = offset
|
23
25
|
@timestamp_delta = timestamp_delta
|
24
26
|
@create_time = create_time
|
25
27
|
@is_control_record = is_control_record
|
@@ -47,6 +49,13 @@ module Kafka
|
|
47
49
|
encoder.write_varint_bytes(record_buffer.string)
|
48
50
|
end
|
49
51
|
|
52
|
+
def ==(other)
|
53
|
+
offset_delta == other.offset_delta &&
|
54
|
+
timestamp_delta == other.timestamp_delta &&
|
55
|
+
offset == other.offset &&
|
56
|
+
is_control_record == other.is_control_record
|
57
|
+
end
|
58
|
+
|
50
59
|
def self.decode(decoder)
|
51
60
|
record_decoder = Decoder.from_string(decoder.varint_bytes)
|
52
61
|
|
@@ -30,7 +30,7 @@ module Kafka
|
|
30
30
|
first_sequence: 0,
|
31
31
|
max_timestamp: Time.now
|
32
32
|
)
|
33
|
-
@records = records
|
33
|
+
@records = Array(records)
|
34
34
|
@first_offset = first_offset
|
35
35
|
@first_timestamp = first_timestamp
|
36
36
|
@codec_id = codec_id
|
@@ -55,6 +55,10 @@ module Kafka
|
|
55
55
|
@records.size
|
56
56
|
end
|
57
57
|
|
58
|
+
def last_offset
|
59
|
+
@first_offset + @last_offset_delta
|
60
|
+
end
|
61
|
+
|
58
62
|
def attributes
|
59
63
|
0x0000 | @codec_id |
|
60
64
|
(@in_transaction ? IN_TRANSACTION_MASK : 0x0) |
|
@@ -131,6 +135,18 @@ module Kafka
|
|
131
135
|
@last_offset_delta = records.length - 1
|
132
136
|
end
|
133
137
|
|
138
|
+
def ==(other)
|
139
|
+
records == other.records &&
|
140
|
+
first_offset == other.first_offset &&
|
141
|
+
partition_leader_epoch == other.partition_leader_epoch &&
|
142
|
+
in_transaction == other.in_transaction &&
|
143
|
+
is_control_batch == other.is_control_batch &&
|
144
|
+
last_offset_delta == other.last_offset_delta &&
|
145
|
+
producer_id == other.producer_id &&
|
146
|
+
producer_epoch == other.producer_epoch &&
|
147
|
+
first_sequence == other.first_sequence
|
148
|
+
end
|
149
|
+
|
134
150
|
def self.decode(decoder)
|
135
151
|
first_offset = decoder.int64
|
136
152
|
|
data/lib/kafka/ssl_context.rb
CHANGED
@@ -4,21 +4,35 @@ require "openssl"
|
|
4
4
|
|
5
5
|
module Kafka
|
6
6
|
module SslContext
|
7
|
+
CLIENT_CERT_DELIMITER = "\n-----END CERTIFICATE-----\n"
|
7
8
|
|
8
|
-
def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, ca_certs_from_system: nil)
|
9
|
-
return nil unless ca_cert_file_path || ca_cert || client_cert || client_cert_key || ca_certs_from_system
|
9
|
+
def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_chain: nil, ca_certs_from_system: nil)
|
10
|
+
return nil unless ca_cert_file_path || ca_cert || client_cert || client_cert_key || client_cert_chain || ca_certs_from_system
|
10
11
|
|
11
12
|
ssl_context = OpenSSL::SSL::SSLContext.new
|
12
13
|
|
13
14
|
if client_cert && client_cert_key
|
14
|
-
|
15
|
+
context_params = {
|
15
16
|
cert: OpenSSL::X509::Certificate.new(client_cert),
|
16
|
-
key: OpenSSL::PKey.read(client_cert_key)
|
17
|
-
|
17
|
+
key: OpenSSL::PKey.read(client_cert_key),
|
18
|
+
}
|
19
|
+
if client_cert_chain
|
20
|
+
certs = []
|
21
|
+
client_cert_chain.split(CLIENT_CERT_DELIMITER).each do |cert|
|
22
|
+
cert += CLIENT_CERT_DELIMITER
|
23
|
+
certs << OpenSSL::X509::Certificate.new(cert)
|
24
|
+
end
|
25
|
+
context_params[:extra_chain_cert] = certs
|
26
|
+
end
|
27
|
+
ssl_context.set_params(context_params)
|
18
28
|
elsif client_cert && !client_cert_key
|
19
29
|
raise ArgumentError, "Kafka client initialized with `ssl_client_cert` but no `ssl_client_cert_key`. Please provide both."
|
20
30
|
elsif !client_cert && client_cert_key
|
21
31
|
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_key`, but no `ssl_client_cert`. Please provide both."
|
32
|
+
elsif client_cert_chain && !client_cert
|
33
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_chain`, but no `ssl_client_cert`. Please provide cert, key and chain."
|
34
|
+
elsif client_cert_chain && !client_cert_key
|
35
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_chain`, but no `ssl_client_cert_key`. Please provide cert, key and chain."
|
22
36
|
end
|
23
37
|
|
24
38
|
if ca_cert || ca_cert_file_path || ca_certs_from_system
|
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kafka/transaction_state_machine'
|
4
|
+
|
5
|
+
module Kafka
|
6
|
+
class TransactionManager
|
7
|
+
DEFAULT_TRANSACTION_TIMEOUT = 60 # 60 seconds
|
8
|
+
TRANSACTION_RESULT_COMMIT = true
|
9
|
+
TRANSACTION_RESULT_ABORT = false
|
10
|
+
|
11
|
+
attr_reader :producer_id, :producer_epoch, :transactional_id
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
cluster:,
|
15
|
+
logger:,
|
16
|
+
idempotent: false,
|
17
|
+
transactional: false,
|
18
|
+
transactional_id: nil,
|
19
|
+
transactional_timeout: DEFAULT_TRANSACTION_TIMEOUT
|
20
|
+
)
|
21
|
+
@cluster = cluster
|
22
|
+
@logger = logger
|
23
|
+
|
24
|
+
@transactional = transactional
|
25
|
+
@transactional_id = transactional_id
|
26
|
+
@transactional_timeout = transactional_timeout
|
27
|
+
@transaction_state = Kafka::TransactionStateMachine.new(logger: logger)
|
28
|
+
@transaction_partitions = {}
|
29
|
+
|
30
|
+
# If transactional mode is enabled, idempotent must be enabled
|
31
|
+
@idempotent = transactional || idempotent
|
32
|
+
|
33
|
+
@producer_id = -1
|
34
|
+
@producer_epoch = 0
|
35
|
+
|
36
|
+
@sequences = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def idempotent?
|
40
|
+
@idempotent == true
|
41
|
+
end
|
42
|
+
|
43
|
+
def transactional?
|
44
|
+
@transactional == true && !@transactional_id.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def init_producer_id(force = false)
|
48
|
+
return if @producer_id >= 0 && !force
|
49
|
+
|
50
|
+
response = transaction_coordinator.init_producer_id(
|
51
|
+
transactional_id: @transactional_id,
|
52
|
+
transactional_timeout: @transactional_timeout
|
53
|
+
)
|
54
|
+
Protocol.handle_error(response.error_code)
|
55
|
+
|
56
|
+
# Reset producer id
|
57
|
+
@producer_id = response.producer_id
|
58
|
+
@producer_epoch = response.producer_epoch
|
59
|
+
|
60
|
+
# Reset sequence
|
61
|
+
@sequences = {}
|
62
|
+
|
63
|
+
@logger.debug "Current Producer ID is #{@producer_id} and Producer Epoch is #{@producer_epoch}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def next_sequence_for(topic, partition)
|
67
|
+
@sequences[topic] ||= {}
|
68
|
+
@sequences[topic][partition] ||= 0
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_sequence_for(topic, partition, sequence)
|
72
|
+
@sequences[topic] ||= {}
|
73
|
+
@sequences[topic][partition] = sequence
|
74
|
+
end
|
75
|
+
|
76
|
+
def init_transactions
|
77
|
+
force_transactional!
|
78
|
+
unless @transaction_state.uninitialized?
|
79
|
+
@logger.warn("Transaction already initialized!")
|
80
|
+
return
|
81
|
+
end
|
82
|
+
init_producer_id(true)
|
83
|
+
@transaction_partitions = {}
|
84
|
+
@transaction_state.transition_to!(TransactionStateMachine::READY)
|
85
|
+
|
86
|
+
@logger.info "Transaction #{@transactional_id} is initialized, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
87
|
+
|
88
|
+
nil
|
89
|
+
rescue
|
90
|
+
@transaction_state.transition_to!(TransactionStateMachine::ERROR)
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_partitions_to_transaction(topic_partitions)
|
95
|
+
force_transactional!
|
96
|
+
|
97
|
+
if @transaction_state.uninitialized?
|
98
|
+
raise 'Transaction is uninitialized'
|
99
|
+
end
|
100
|
+
|
101
|
+
# Extract newly created partitions
|
102
|
+
new_topic_partitions = {}
|
103
|
+
topic_partitions.each do |topic, partitions|
|
104
|
+
partitions.each do |partition|
|
105
|
+
@transaction_partitions[topic] ||= {}
|
106
|
+
if !@transaction_partitions[topic][partition]
|
107
|
+
new_topic_partitions[topic] ||= []
|
108
|
+
new_topic_partitions[topic] << partition
|
109
|
+
|
110
|
+
@logger.info "Adding parition #{topic}/#{partition} to transaction #{@transactional_id}, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
unless new_topic_partitions.empty?
|
116
|
+
response = transaction_coordinator.add_partitions_to_txn(
|
117
|
+
transactional_id: @transactional_id,
|
118
|
+
producer_id: @producer_id,
|
119
|
+
producer_epoch: @producer_epoch,
|
120
|
+
topics: new_topic_partitions
|
121
|
+
)
|
122
|
+
|
123
|
+
# Update added topic partitions
|
124
|
+
response.errors.each do |tp|
|
125
|
+
tp.partitions.each do |p|
|
126
|
+
Protocol.handle_error(p.error_code)
|
127
|
+
@transaction_partitions[tp.topic] ||= {}
|
128
|
+
@transaction_partitions[tp.topic][p.partition] = true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
nil
|
134
|
+
rescue
|
135
|
+
@transaction_state.transition_to!(TransactionStateMachine::ERROR)
|
136
|
+
raise
|
137
|
+
end
|
138
|
+
|
139
|
+
def begin_transaction
|
140
|
+
force_transactional!
|
141
|
+
raise 'Transaction has already started' if @transaction_state.in_transaction?
|
142
|
+
raise 'Transaction is not ready' unless @transaction_state.ready?
|
143
|
+
@transaction_state.transition_to!(TransactionStateMachine::IN_TRANSACTION)
|
144
|
+
|
145
|
+
@logger.info "Begin transaction #{@transactional_id}, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
146
|
+
|
147
|
+
nil
|
148
|
+
rescue
|
149
|
+
@transaction_state.transition_to!(TransactionStateMachine::ERROR)
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
|
153
|
+
def commit_transaction
|
154
|
+
force_transactional!
|
155
|
+
|
156
|
+
if @transaction_state.committing_transaction?
|
157
|
+
@logger.warn("Transaction is being committed")
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
unless @transaction_state.in_transaction?
|
162
|
+
raise 'Transaction is not valid to commit'
|
163
|
+
end
|
164
|
+
|
165
|
+
@transaction_state.transition_to!(TransactionStateMachine::COMMITTING_TRANSACTION)
|
166
|
+
|
167
|
+
@logger.info "Commiting transaction #{@transactional_id}, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
168
|
+
|
169
|
+
response = transaction_coordinator.end_txn(
|
170
|
+
transactional_id: @transactional_id,
|
171
|
+
producer_id: @producer_id,
|
172
|
+
producer_epoch: @producer_epoch,
|
173
|
+
transaction_result: TRANSACTION_RESULT_COMMIT
|
174
|
+
)
|
175
|
+
Protocol.handle_error(response.error_code)
|
176
|
+
|
177
|
+
@logger.info "Transaction #{@transactional_id} is committed, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
178
|
+
complete_transaction
|
179
|
+
|
180
|
+
nil
|
181
|
+
rescue
|
182
|
+
@transaction_state.transition_to!(TransactionStateMachine::ERROR)
|
183
|
+
raise
|
184
|
+
end
|
185
|
+
|
186
|
+
def abort_transaction
|
187
|
+
force_transactional!
|
188
|
+
|
189
|
+
if @transaction_state.aborting_transaction?
|
190
|
+
@logger.warn("Transaction is being aborted")
|
191
|
+
return
|
192
|
+
end
|
193
|
+
|
194
|
+
unless @transaction_state.in_transaction?
|
195
|
+
raise 'Transaction is not valid to abort'
|
196
|
+
end
|
197
|
+
|
198
|
+
@transaction_state.transition_to!(TransactionStateMachine::ABORTING_TRANSACTION)
|
199
|
+
|
200
|
+
@logger.info "Aborting transaction #{@transactional_id}, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
201
|
+
|
202
|
+
response = transaction_coordinator.end_txn(
|
203
|
+
transactional_id: @transactional_id,
|
204
|
+
producer_id: @producer_id,
|
205
|
+
producer_epoch: @producer_epoch,
|
206
|
+
transaction_result: TRANSACTION_RESULT_ABORT
|
207
|
+
)
|
208
|
+
Protocol.handle_error(response.error_code)
|
209
|
+
|
210
|
+
@logger.info "Transaction #{@transactional_id} is aborted, Producer ID: #{@producer_id} (Epoch #{@producer_epoch})"
|
211
|
+
|
212
|
+
complete_transaction
|
213
|
+
|
214
|
+
nil
|
215
|
+
rescue
|
216
|
+
@transaction_state.transition_to!(TransactionStateMachine::ERROR)
|
217
|
+
raise
|
218
|
+
end
|
219
|
+
|
220
|
+
def in_transaction?
|
221
|
+
@transaction_state.in_transaction?
|
222
|
+
end
|
223
|
+
|
224
|
+
def error?
|
225
|
+
@transaction_state.error?
|
226
|
+
end
|
227
|
+
|
228
|
+
def close
|
229
|
+
if in_transaction?
|
230
|
+
@logger.warn("Aborting pending transaction ...")
|
231
|
+
abort_transaction
|
232
|
+
elsif @transaction_state.aborting_transaction? || @transaction_state.committing_transaction?
|
233
|
+
@logger.warn("Transaction is finishing. Sleeping until finish!")
|
234
|
+
sleep 5
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def force_transactional!
|
241
|
+
unless transactional?
|
242
|
+
raise 'Please turn on transactional mode to use transaction'
|
243
|
+
end
|
244
|
+
|
245
|
+
if @transactional_id.nil? || @transactional_id.empty?
|
246
|
+
raise 'Please provide a transaction_id to use transactional mode'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def transaction_coordinator
|
251
|
+
@cluster.get_transaction_coordinator(
|
252
|
+
transactional_id: @transactional_id
|
253
|
+
)
|
254
|
+
end
|
255
|
+
|
256
|
+
def complete_transaction
|
257
|
+
@transaction_state.transition_to!(TransactionStateMachine::READY)
|
258
|
+
@transaction_partitions = {}
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|