pulsar_sdk 0.8.8

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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +51 -0
  3. data/Gemfile +6 -0
  4. data/LICENSE +201 -0
  5. data/README.md +107 -0
  6. data/Rakefile +2 -0
  7. data/bin/console +15 -0
  8. data/bin/setup +8 -0
  9. data/lib/protobuf/pulsar_api.pb.rb +710 -0
  10. data/lib/protobuf/pulsar_api.proto +934 -0
  11. data/lib/protobuf/validate.rb +41 -0
  12. data/lib/pulsar_admin.rb +14 -0
  13. data/lib/pulsar_admin/api.rb +215 -0
  14. data/lib/pulsar_sdk.rb +55 -0
  15. data/lib/pulsar_sdk/client.rb +13 -0
  16. data/lib/pulsar_sdk/client/connection.rb +371 -0
  17. data/lib/pulsar_sdk/client/connection_pool.rb +79 -0
  18. data/lib/pulsar_sdk/client/rpc.rb +67 -0
  19. data/lib/pulsar_sdk/consumer.rb +13 -0
  20. data/lib/pulsar_sdk/consumer/base.rb +148 -0
  21. data/lib/pulsar_sdk/consumer/manager.rb +127 -0
  22. data/lib/pulsar_sdk/consumer/message_tracker.rb +86 -0
  23. data/lib/pulsar_sdk/options.rb +6 -0
  24. data/lib/pulsar_sdk/options/base.rb +10 -0
  25. data/lib/pulsar_sdk/options/connection.rb +51 -0
  26. data/lib/pulsar_sdk/options/consumer.rb +34 -0
  27. data/lib/pulsar_sdk/options/producer.rb +14 -0
  28. data/lib/pulsar_sdk/options/reader.rb +7 -0
  29. data/lib/pulsar_sdk/options/tls.rb +8 -0
  30. data/lib/pulsar_sdk/producer.rb +14 -0
  31. data/lib/pulsar_sdk/producer/base.rb +154 -0
  32. data/lib/pulsar_sdk/producer/manager.rb +67 -0
  33. data/lib/pulsar_sdk/producer/message.rb +47 -0
  34. data/lib/pulsar_sdk/producer/router.rb +100 -0
  35. data/lib/pulsar_sdk/protocol.rb +8 -0
  36. data/lib/pulsar_sdk/protocol/frame.rb +53 -0
  37. data/lib/pulsar_sdk/protocol/lookup.rb +55 -0
  38. data/lib/pulsar_sdk/protocol/message.rb +55 -0
  39. data/lib/pulsar_sdk/protocol/namespace.rb +22 -0
  40. data/lib/pulsar_sdk/protocol/partitioned.rb +54 -0
  41. data/lib/pulsar_sdk/protocol/reader.rb +67 -0
  42. data/lib/pulsar_sdk/protocol/structure.rb +93 -0
  43. data/lib/pulsar_sdk/protocol/topic.rb +74 -0
  44. data/lib/pulsar_sdk/tweaks.rb +10 -0
  45. data/lib/pulsar_sdk/tweaks/assign_attributes.rb +30 -0
  46. data/lib/pulsar_sdk/tweaks/base_command.rb +66 -0
  47. data/lib/pulsar_sdk/tweaks/binary_heap.rb +133 -0
  48. data/lib/pulsar_sdk/tweaks/clean_inspect.rb +15 -0
  49. data/lib/pulsar_sdk/tweaks/time_at_microsecond.rb +27 -0
  50. data/lib/pulsar_sdk/tweaks/timeout_queue.rb +52 -0
  51. data/lib/pulsar_sdk/tweaks/wait_map.rb +81 -0
  52. data/lib/pulsar_sdk/version.rb +3 -0
  53. data/pulsar_sdk.gemspec +31 -0
  54. metadata +151 -0
@@ -0,0 +1,79 @@
1
+ module PulsarSdk
2
+ module Client
3
+ class ConnectionPool
4
+ prepend ::PulsarSdk::Tweaks::CleanInspect
5
+
6
+ def initialize(opts)
7
+ raise "opts expected a PulsarSdk::Options::Connection got #{opts.class}" unless opts.is_a?(PulsarSdk::Options::Connection)
8
+
9
+ @mutex = Mutex.new
10
+ @pool = ::PulsarSdk::Tweaks::WaitMap.new
11
+
12
+ @options = opts
13
+ @keepalive = opts.keepalive
14
+ @connection_timeout = opts.connection_timeout
15
+
16
+ @authentication = opts.auth_provider
17
+ @tls_options = opts.tls_options
18
+
19
+ instance_variables.each do |x|
20
+ remove_instance_variable(x) if instance_variable_get(x).nil?
21
+ end
22
+ end
23
+
24
+ def fetch(logical_addr, physical_addr)
25
+ id = (logical_addr || physical_addr).to_s
26
+ raise 'logical_addr and physical_addr both empty!' if id.empty?
27
+
28
+ conn = nil
29
+ @mutex.synchronize do
30
+ conn = @pool.find(id)
31
+
32
+ if conn.nil? || conn.closed?
33
+ # REMOVE closed conncetion from pool
34
+ @pool.delete(id, 0.01) unless conn.nil?
35
+
36
+ opts = @options.dup
37
+ opts.assign_attributes(
38
+ logical_addr: logical_addr,
39
+ physical_addr: physical_addr
40
+ )
41
+
42
+ conn = @pool.add(id, ::PulsarSdk::Client::Connection.establish(opts))
43
+ end
44
+ end
45
+
46
+ conn
47
+ end
48
+
49
+ def run_checker
50
+ Thread.new do
51
+ loop do
52
+ begin
53
+ @pool.each do |_k, v|
54
+ last_ping_at, last_received_at = v.active_status
55
+
56
+ case
57
+ when last_ping_at - last_received_at >= @keepalive * 2
58
+ v.close
59
+ when last_ping_at - last_received_at > @keepalive
60
+ v.ping
61
+ end
62
+ end
63
+ rescue => exp
64
+ PulsarSdk.logger.error(exp)
65
+ end
66
+
67
+ sleep(1)
68
+ end
69
+ end
70
+ end
71
+
72
+ def close
73
+ @pool.clear do |_, v|
74
+ v.close
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,67 @@
1
+ module PulsarSdk
2
+ module Client
3
+ class Rpc
4
+ prepend ::PulsarSdk::Tweaks::CleanInspect
5
+
6
+ def initialize(opts)
7
+ raise "opts expected a PulsarSdk::Options::Connection got #{opts.class}" unless opts.is_a?(PulsarSdk::Options::Connection)
8
+
9
+ @opts = opts
10
+
11
+ @cnx = ::PulsarSdk::Client::ConnectionPool.new(opts).tap {|x| x.run_checker}
12
+
13
+ @producer_id = 0
14
+ @consumer_id = 0
15
+ end
16
+
17
+ def connection(logical_addr = nil, physical_addr = nil)
18
+ logical_addr ||= @opts.logical_addr
19
+ @cnx.fetch(logical_addr, physical_addr)
20
+ end
21
+
22
+ def lookup(topic)
23
+ @lookup_service ||= ::PulsarSdk::Protocol::Lookup.new(self, @opts.logical_addr)
24
+ @lookup_service.lookup(topic)
25
+ end
26
+
27
+ def namespace_topics(namespace)
28
+ @namespace_service ||= ::PulsarSdk::Protocol::Namespace.new(self)
29
+ @namespace_service.topics(namespace)
30
+ end
31
+
32
+ def partition_topics(topic)
33
+ ::PulsarSdk::Protocol::Partitioned.new(self, topic)&.partitions || []
34
+ end
35
+
36
+ def request(physical_addr, logical_addr, cmd)
37
+ connection(physical_addr, logical_addr).request(cmd, nil, true)
38
+ end
39
+
40
+ def request_any_broker(cmd)
41
+ connection.request(cmd)
42
+ end
43
+
44
+ def close
45
+ @cnx.close
46
+ end
47
+
48
+ def create_producer(opts)
49
+ raise "opts expected a PulsarSdk::Options::Producer got #{opts.class}" unless opts.is_a?(PulsarSdk::Options::Producer)
50
+ # FIXME check if connection ready
51
+ ::PulsarSdk::Producer.create(self, opts)
52
+ end
53
+
54
+ def subscribe(opts)
55
+ raise "opts expected a PulsarSdk::Options::Consumer got #{opts.class}" unless opts.is_a?(PulsarSdk::Options::Consumer)
56
+ # FIXME check if connection ready
57
+ consumer = ::PulsarSdk::Consumer.create(self, opts)
58
+
59
+ consumer
60
+ end
61
+
62
+ def create_reader(opts = {})
63
+
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,13 @@
1
+ require 'pulsar_sdk/consumer/base'
2
+ require 'pulsar_sdk/consumer/message_tracker'
3
+ require 'pulsar_sdk/consumer/manager'
4
+
5
+ module PulsarSdk
6
+ module Consumer
7
+ extend self
8
+
9
+ def create(client, opts)
10
+ PulsarSdk::Consumer::Manager.new(client, opts)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,148 @@
1
+ module PulsarSdk
2
+ module Consumer
3
+ class Base
4
+ attr_reader :consumer_id, :topic
5
+
6
+ def initialize(client, message_tracker, opts)
7
+ @opts = opts
8
+ @topic = @opts.topic
9
+ @message_tracker = message_tracker
10
+ @client = client
11
+ end
12
+
13
+ def grab_cnx
14
+ @prefetch = @opts.prefetch
15
+ @fetched = 0
16
+ @capacity = 0
17
+
18
+ @conn = @client.connection(*@client.lookup(@topic))
19
+ @established = true
20
+
21
+ @seq_generator = SeqGenerator.new(@conn.seq_generator)
22
+
23
+ @consumer_id = @seq_generator.new_consumer_id
24
+ @consumer_name = @opts.name
25
+ @subscription_name = @opts.subscription_name
26
+
27
+ result = init_consumer
28
+ @consumer_name = result.consumerName unless result.nil?
29
+ end
30
+
31
+ def increase_fetched(n = 1)
32
+ @fetched += n
33
+ end
34
+
35
+ def flow
36
+ base_cmd = Pulsar::Proto::BaseCommand.new(
37
+ type: Pulsar::Proto::BaseCommand::Type::FLOW,
38
+ flow: Pulsar::Proto::CommandFlow.new(
39
+ messagePermits: @prefetch
40
+ )
41
+ )
42
+
43
+ execute(base_cmd)
44
+
45
+ @capacity += @prefetch
46
+ end
47
+
48
+ def subscription
49
+ @subscription_name
50
+ end
51
+
52
+ def unsubscribe
53
+ base_cmd = Pulsar::Proto::BaseCommand.new(
54
+ type: Pulsar::Proto::BaseCommand::Type::UNSUBSCRIBE,
55
+ unsubscribe: Pulsar::Proto::CommandUnsubscribe.new
56
+ )
57
+ execute_async(base_cmd)
58
+ end
59
+
60
+ def flow_if_need
61
+ return if @capacity > 0 && [@prefetch / 2, 1].max.ceil < (@capacity - @fetched)
62
+ flow
63
+ end
64
+
65
+ def close
66
+ base_cmd = Pulsar::Proto::BaseCommand.new(
67
+ type: Pulsar::Proto::BaseCommand::Type::CLOSE_CONSUMER,
68
+ close_consumer: Pulsar::Proto::CommandCloseConsumer.new(
69
+ consumer_id: @consumer_id
70
+ )
71
+ )
72
+ execute(base_cmd) unless disconnect?
73
+
74
+ remove_handler!
75
+ end
76
+
77
+ def disconnect?
78
+ !@established
79
+ end
80
+
81
+ def execute(cmd)
82
+ write(cmd)
83
+ end
84
+
85
+ def execute_async(cmd)
86
+ write(cmd, nil, true)
87
+ end
88
+
89
+ private
90
+ def write(cmd, *args)
91
+ grab_cnx if disconnect?
92
+ cmd.seq_generator = @seq_generator
93
+
94
+ @conn.request(cmd, *args)
95
+ end
96
+
97
+ def bind_handler!
98
+ handler = Proc.new do |cmd, meta_and_payload|
99
+ cmd.nil? ? (@established = false) : @message_tracker.receive(cmd, meta_and_payload)
100
+ end
101
+ @conn.consumer_handlers.add(@consumer_id, handler)
102
+ end
103
+
104
+ def remove_handler!
105
+ @conn.consumer_handlers.delete(@consumer_id)
106
+
107
+ true
108
+ end
109
+
110
+ def init_consumer
111
+ bind_handler!
112
+
113
+ base_cmd = Pulsar::Proto::BaseCommand.new(
114
+ type: Pulsar::Proto::BaseCommand::Type::SUBSCRIBE,
115
+ subscribe: Pulsar::Proto::CommandSubscribe.new(
116
+ topic: @opts.topic,
117
+ subscription: @opts.subscription_name,
118
+ subType: @opts.subscription_type,
119
+ consumer_name: @consumer_name,
120
+ replicate_subscription_state: @opts.replicate_subscription_state,
121
+ read_compacted: @opts.read_compacted
122
+ )
123
+ )
124
+ result = execute(base_cmd)
125
+
126
+ @message_tracker.add_consumer(self)
127
+
128
+ result.consumerStatsResponse
129
+ end
130
+
131
+ # NOTE keep consumer_id and sequence_id static
132
+ class SeqGenerator
133
+ def initialize(seq_g)
134
+ @seq_g = seq_g
135
+ @consumer_id = @seq_g.new_consumer_id
136
+ end
137
+
138
+ def new_consumer_id
139
+ @consumer_id
140
+ end
141
+
142
+ def method_missing(method)
143
+ @seq_g.public_send(method)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,127 @@
1
+ module PulsarSdk
2
+ module Consumer
3
+ class Manager
4
+ prepend ::PulsarSdk::Tweaks::CleanInspect
5
+
6
+ def initialize(client, opts)
7
+ raise "client expected a PulsarSdk::Client::Rpc got #{client.class}" unless client.is_a?(PulsarSdk::Client::Rpc)
8
+ raise "opts expected a PulsarSdk::Options::Consumer got #{opts.class}" unless opts.is_a?(PulsarSdk::Options::Consumer)
9
+
10
+ @topic = opts.topic
11
+
12
+ @listen_wait = opts.listen_wait
13
+
14
+ @message_tracker = ::PulsarSdk::Consumer::MessageTracker.new(opts.redelivery_delay)
15
+
16
+ @consumers = init_consumer_by(client, opts)
17
+
18
+ @stoped = false
19
+ end
20
+
21
+ # NOTE some topic maybe have large permits if there is no message
22
+ def flow
23
+ ensure_connection
24
+ @consumers.each(&:flow_if_need)
25
+ end
26
+
27
+ # NOTE all consumers has same name
28
+ def subscription
29
+ ensure_connection
30
+ @consumers.find(&:subscription)
31
+ end
32
+
33
+ def unsubscribe
34
+ ensure_connection
35
+ @consumers.each(&:unsubscribe)
36
+ end
37
+
38
+ # if timeout is nil wait until get message
39
+ def receive(timeout = nil)
40
+ ensure_connection
41
+ @message_tracker.shift(timeout)
42
+ end
43
+
44
+ def listen(autoack = false)
45
+ raise 'listen require passing a block!!' if !block_given?
46
+ ensure_connection
47
+
48
+ loop do
49
+ return if @stoped
50
+
51
+ flow
52
+
53
+ cmd, msg = receive(@listen_wait)
54
+ return if msg.nil?
55
+
56
+ result = yield cmd, msg
57
+
58
+ if autoack && result == false
59
+ msg.nack
60
+ next
61
+ end
62
+
63
+ msg.ack if autoack
64
+ end
65
+ end
66
+
67
+ def close
68
+ PulsarSdk.logger.debug(__method__){"current @stoped #{@stoped} close now!"}
69
+ return if @stoped
70
+ @consumers.each(&:close)
71
+ @stoped = true
72
+
73
+ @message_tracker.close
74
+ end
75
+
76
+ private
77
+ def init_consumer_by(client, opts)
78
+ topics = []
79
+
80
+ case
81
+ when !opts.topic.nil?
82
+ PulsarSdk.logger.debug("#{__method__}:single topic"){opts.topic}
83
+
84
+ topics << ::PulsarSdk::Protocol::Topic.parse(opts.topic).to_s
85
+ when !Array(opts.topics).size.zero?
86
+ PulsarSdk.logger.debug("#{__method__}:multiple topics"){opts.topics}
87
+
88
+ opts.topics.each do |topic|
89
+ topics << ::PulsarSdk::Protocol::Topic.parse(topic).to_s
90
+ end
91
+ when !opts.topics_pattern.nil?
92
+ PulsarSdk.logger.debug("#{__method__}:pattern topic"){opts.topics_pattern}
93
+
94
+ tn = ::PulsarSdk::Protocol::Topic.parse(opts.topics_pattern)
95
+ pattern = Regexp.compile(tn.topic == '*' ? '^*' : tn.topic)
96
+ client.namespace_topics(tn.namespace).each do |topic|
97
+ topics << topic if pattern.match(topic)
98
+ end
99
+ else
100
+ raise 'You must provide one topic by 「topic」or「topics」or「topics_pattern」'
101
+ end
102
+
103
+ PulsarSdk.logger.debug("#{__method__}:topics to initialize"){topics}
104
+
105
+ topics.flat_map do |topic|
106
+ partition_topics = client.partition_topics(topic)
107
+
108
+ partition_topics.map do |x|
109
+ opts_ = opts.dup
110
+
111
+ opts_.topic = x
112
+ PulsarSdk::Consumer::Base.new(client, @message_tracker, opts_).tap do |consumer|
113
+ consumer.grab_cnx
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def ensure_connection
120
+ @consumers.each do |consumer|
121
+ next unless consumer.disconnect?
122
+ consumer.grab_cnx
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,86 @@
1
+ module PulsarSdk
2
+ module Consumer
3
+ class MessageTracker
4
+
5
+ class ReceivedQueue < PulsarSdk::Tweaks::TimeoutQueue; end
6
+ class NackQueue < PulsarSdk::Tweaks::BinaryHeap; end
7
+
8
+ def initialize(redelivery_delay)
9
+ @redelivery_delay = redelivery_delay
10
+ @received_message = ReceivedQueue.new
11
+ @acknowledge_message = NackQueue.new {|parent, child| child[:ack_at] <=> parent[:ack_at] }
12
+ @consumers = {}
13
+
14
+ @tracker = track
15
+ end
16
+
17
+ def add_consumer(consumer)
18
+ @consumers[consumer.consumer_id] = consumer
19
+ end
20
+
21
+ def receive(*args)
22
+ @received_message.add(*args)
23
+ end
24
+
25
+ def shift(timeout)
26
+ cmd, meta_and_payload = @received_message.pop(timeout)
27
+
28
+ return if cmd.nil?
29
+
30
+ message = PulsarSdk::Protocol::Structure.new(meta_and_payload).decode
31
+
32
+ consumer_id = cmd.message&.consumer_id
33
+ real_consumer = @consumers[consumer_id]
34
+
35
+ message.assign_attributes(
36
+ message_id: cmd.message&.message_id,
37
+ consumer_id: consumer_id,
38
+ topic: real_consumer&.topic,
39
+ ack_handler: ack_handler
40
+ )
41
+
42
+ real_consumer&.increase_fetched
43
+
44
+ [cmd, message]
45
+ end
46
+
47
+ def close
48
+ @received_message.close
49
+ end
50
+
51
+ private
52
+ def track
53
+ Thread.new do
54
+ loop do
55
+ while item = @acknowledge_message.top
56
+ break if item[:ack_at] > Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
+ begin
58
+ PulsarSdk.logger.debug('acknowledge message'){"#{Process.clock_gettime(Process::CLOCK_MONOTONIC)}: #{item[:cmd].type} --> #{item[:ack_at]}"}
59
+ execute_async(item[:cmd])
60
+ @acknowledge_message.shift
61
+ rescue => exp
62
+ PulsarSdk.logger.error('Error occur when acknowledge message'){exp}
63
+ PulsarSdk.logger.error('Error occur when acknowledge message'){item}
64
+ end
65
+ end
66
+ sleep(1)
67
+ end
68
+ end
69
+ end
70
+
71
+ def ack_handler
72
+ Proc.new do |cmd|
73
+ current_clock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
74
+ ack_at = cmd.typeof_ack? ? (current_clock - 1) : (current_clock + @redelivery_delay.to_i)
75
+ @acknowledge_message.insert(cmd: cmd, ack_at: ack_at)
76
+ end
77
+ end
78
+
79
+ def execute_async(cmd)
80
+ consumer = @consumers[cmd.get_consumer_id]
81
+
82
+ consumer.execute_async(cmd)
83
+ end
84
+ end
85
+ end
86
+ end