pulsar_sdk 0.8.8

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