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,8 @@
1
+ require 'pulsar_sdk/protocol/frame'
2
+ require 'pulsar_sdk/protocol/message'
3
+ require 'pulsar_sdk/protocol/structure'
4
+ require 'pulsar_sdk/protocol/reader'
5
+ require 'pulsar_sdk/protocol/lookup'
6
+ require 'pulsar_sdk/protocol/namespace'
7
+ require 'pulsar_sdk/protocol/topic'
8
+ require 'pulsar_sdk/protocol/partitioned'
@@ -0,0 +1,53 @@
1
+ require 'digest/crc32c'
2
+
3
+ module PulsarSdk
4
+ module Protocol
5
+ class Frame
6
+ # 预留4byte存放帧长度
7
+ PREPENDED_SIZE = 4
8
+ CHECKSUM_SIZE = 4
9
+ MAGIC_NUMBER = [0x0e, 0x01].pack('C*').freeze
10
+
11
+ def self.encode(command, message = nil)
12
+ raise "command MUST be Pulsar::Proto::BaseCommand but got #{command.class}" unless command.is_a?(Pulsar::Proto::BaseCommand)
13
+
14
+ pb_cmd = command.to_proto
15
+
16
+ # 非发送消息帧
17
+ return encode_command(pb_cmd) if message.nil?
18
+
19
+ # 消息发送帧
20
+ # [TOTAL_SIZE] [CMD_SIZE] [CMD] [MAGIC_NUMBER] [CHECKSUM] [METADATA_SIZE] [METADATA] [PAYLOAD]
21
+ raise "message MUST be PulsarSdk::Producer::Message but got #{message.class}" unless message.is_a?(PulsarSdk::Producer::Message)
22
+
23
+ metadata = message.metadata
24
+ pb_meta = metadata.to_proto
25
+
26
+ meta_payload = binary(pb_meta.size) + pb_meta + message.binary_string
27
+ checksum = crc32(meta_payload)
28
+
29
+ total_size = PREPENDED_SIZE + pb_cmd.size + MAGIC_NUMBER.size + CHECKSUM_SIZE + meta_payload.size
30
+
31
+ binary(total_size, pb_cmd.size) + pb_cmd + MAGIC_NUMBER + binary(checksum) + meta_payload
32
+ end
33
+
34
+ def self.encode_command(pb_cmd)
35
+ binary(pb_cmd.size + PREPENDED_SIZE, pb_cmd.size) + pb_cmd
36
+ end
37
+
38
+ def self.decode(byte)
39
+ Pulsar::Proto::BaseCommand.decode(byte)
40
+ end
41
+
42
+ def self.binary(*obj)
43
+ obj.map { |x| Array(x).pack('N') }.join
44
+ end
45
+
46
+ def self.crc32(bytes)
47
+ crc = Digest::CRC32c.new
48
+ crc << bytes
49
+ crc.checksum
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Lookup
4
+ MAX_LOOKUP_TIMES = 20
5
+
6
+ def initialize(client, service_url)
7
+ @client = client
8
+ @service_url = service_url
9
+ end
10
+
11
+ # output
12
+ # [logical_addr, physical_addr]
13
+ def lookup(topic)
14
+ base_cmd = Pulsar::Proto::BaseCommand.new(
15
+ type: Pulsar::Proto::BaseCommand::Type::LOOKUP,
16
+ lookupTopic: Pulsar::Proto::CommandLookupTopic.new(
17
+ topic: topic,
18
+ authoritative: false
19
+ )
20
+ )
21
+ resp = @client.request_any_broker(base_cmd).lookupTopicResponse
22
+
23
+ # 最多查找这么多次
24
+ MAX_LOOKUP_TIMES.times do
25
+ case Pulsar::Proto::CommandLookupTopicResponse::LookupType.resolve(resp.response)
26
+ when Pulsar::Proto::CommandLookupTopicResponse::LookupType::Failed
27
+ PulsarSdk.logger.error(__method__){"Failed to lookup topic 「#{topic}」, #{resp.error}"}
28
+ break
29
+ when Pulsar::Proto::CommandLookupTopicResponse::LookupType::Redirect
30
+ logical_addr, physical_addr = extract_addr(resp)
31
+ base_cmd = Pulsar::Proto::BaseCommand.new(
32
+ type: Pulsar::Proto::BaseCommand::Type::LOOKUP,
33
+ lookupTopic: Pulsar::Proto::CommandLookupTopic.new(
34
+ topic: topic,
35
+ authoritative: resp.authoritative
36
+ )
37
+ )
38
+ # NOTE 从连接池拿
39
+ resp = @client.request(logical_addr, physical_addr, base_cmd).lookupTopicResponse
40
+ when Pulsar::Proto::CommandLookupTopicResponse::LookupType::Connect
41
+ return extract_addr(resp)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+ def extract_addr(resp)
48
+ logical_addr = resp.brokerServiceUrl
49
+ physical_addr = resp.proxy_through_service_url ? @service_url : logical_addr
50
+
51
+ [logical_addr, physical_addr]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Message
4
+ prepend ::PulsarSdk::Tweaks::AssignAttributes
5
+ prepend ::PulsarSdk::Tweaks::CleanInspect
6
+
7
+ attr_accessor :publish_time, :event_time, :partition_key, :payload,
8
+ :message_id, :properties, :consumer_id, :topic
9
+
10
+ attr_accessor :ack_handler
11
+
12
+ # def publish_at
13
+ # def event_at
14
+ [:publish, :event].each do |x|
15
+ define_method "#{x}_at" do
16
+ v = self.public_send("#{x}_time").to_i
17
+ return if v.zero?
18
+ TimeX.at_timestamp(v)
19
+ end
20
+ end
21
+
22
+ def ack(type = Pulsar::Proto::CommandAck::AckType::Individual)
23
+ base_cmd = Pulsar::Proto::BaseCommand.new(
24
+ type: Pulsar::Proto::BaseCommand::Type::ACK,
25
+ ack: Pulsar::Proto::CommandAck.new(
26
+ consumer_id: self.consumer_id,
27
+ message_id: [self.message_id],
28
+ ack_type: type
29
+ )
30
+ )
31
+
32
+ ack_handler.call(base_cmd)
33
+ @confirmed = true
34
+ end
35
+
36
+ # 检查是否有确认,无论是ack还是nack都算是确认
37
+ def confirmed?
38
+ !!@confirmed
39
+ end
40
+
41
+ def nack
42
+ base_cmd = Pulsar::Proto::BaseCommand.new(
43
+ type: Pulsar::Proto::BaseCommand::Type::REDELIVER_UNACKNOWLEDGED_MESSAGES,
44
+ redeliverUnacknowledgedMessages: Pulsar::Proto::CommandRedeliverUnacknowledgedMessages.new(
45
+ consumer_id: self.consumer_id,
46
+ message_ids: [self.message_id]
47
+ )
48
+ )
49
+
50
+ ack_handler.call(base_cmd)
51
+ @confirmed = true
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,22 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Namespace
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def topics(namespace)
9
+ base_cmd = Pulsar::Proto::BaseCommand.new(
10
+ type: Pulsar::Proto::BaseCommand::Type::GET_TOPICS_OF_NAMESPACE,
11
+ getTopicsOfNamespace: Pulsar::Proto::CommandGetTopicsOfNamespace.new(
12
+ namespace: namespace,
13
+ mode: Pulsar::Proto::CommandGetTopicsOfNamespace::Mode.resolve(:ALL)
14
+ )
15
+ )
16
+ resp = @client.request_any_broker(base_cmd)
17
+
18
+ resp.getTopicsOfNamespaceResponse&.topics
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Partitioned
4
+ def initialize(client, topic)
5
+ @client = client
6
+ @tn = ::PulsarSdk::Protocol::Topic.parse(topic)
7
+ end
8
+
9
+ def partitions
10
+ pmr = topic_metadata
11
+
12
+ if !success_response?(pmr)
13
+ PulsarSdk.logger.error(__method__){"Get topic partitioned metadata failed, #{pmr.error}: #{pmr.message}"}
14
+ return []
15
+ end
16
+
17
+ return [@tn.to_s] if pmr.partitions.zero?
18
+
19
+ tn = @tn.dup
20
+ (0..pmr.partitions).map do |i|
21
+ tn.partition = i
22
+ tn.to_s
23
+ end
24
+ end
25
+
26
+ # 当前topic是否是分区topic
27
+ def partitioned?
28
+ topic_metadata.partitions > 0
29
+ end
30
+
31
+ private
32
+ def success_response?(pmr)
33
+ result = false
34
+ Pulsar::Proto::CommandPartitionedTopicMetadataResponse::LookupType.tap do |x|
35
+ result = x.resolve(pmr.response) == x.const_get(:Success)
36
+ end
37
+
38
+ result
39
+ end
40
+
41
+ def topic_metadata
42
+ return @topic_metadata_ unless @topic_metadata_.nil?
43
+
44
+ base_cmd = Pulsar::Proto::BaseCommand.new(
45
+ type: Pulsar::Proto::BaseCommand::Type::PARTITIONED_METADATA,
46
+ partitionMetadata: Pulsar::Proto::CommandPartitionedTopicMetadata.new(
47
+ topic: @tn.to_s
48
+ )
49
+ )
50
+ @topic_metadata_ = @client.request_any_broker(base_cmd).partitionMetadataResponse
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,67 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Reader
4
+ prepend ::PulsarSdk::Tweaks::CleanInspect
5
+
6
+ FRAME_SIZE_LEN = 4
7
+ CMD_SIZE_LEN = 4
8
+
9
+ def initialize(io)
10
+ ensure_interface_implemented!(io)
11
+ @io = io
12
+
13
+ @readed = 0
14
+ end
15
+
16
+ # TODO add timeout?
17
+ def read_fully
18
+ frame_szie = read_frame_size
19
+ raise "IO reader is empty! maybe server error, please check server log for help." if frame_szie.nil?
20
+
21
+ base_cmd = read_command
22
+
23
+ buffer = read_remaining(frame_szie)
24
+
25
+ [base_cmd, buffer]
26
+ end
27
+
28
+ def read_frame_size
29
+ frame_size = read(FRAME_SIZE_LEN, 'N')
30
+ # reset cursor! let's read the frame
31
+ @readed = 0
32
+ frame_size
33
+ end
34
+
35
+ def read_command
36
+ cmd_size = read(CMD_SIZE_LEN, 'N')
37
+ cmd_bytes = read(cmd_size)
38
+ Pulsar::Proto::BaseCommand.decode(cmd_bytes)
39
+ end
40
+
41
+ def read_remaining(frame_szie)
42
+ meta_and_payload_size = frame_szie - @readed
43
+ return if meta_and_payload_size <= 0
44
+ read(meta_and_payload_size)
45
+ end
46
+
47
+ private
48
+ def ensure_interface_implemented!(io)
49
+ [:read, :closed?].each do |x|
50
+ raise "io must implement method: #{x}" unless io.respond_to?(x)
51
+ end
52
+ end
53
+
54
+ def read(size, unpack = nil)
55
+ raise Errno::ECONNRESET if @io.closed?
56
+ raise Errno::ETIMEDOUT unless IO.select([@io], nil)
57
+
58
+ bytes = @io.read(size)
59
+ @readed = @readed.to_i + size.to_i
60
+
61
+ return bytes if unpack.nil? || bytes.nil?
62
+
63
+ bytes.unpack(unpack).first
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,93 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Structure
4
+ prepend ::PulsarSdk::Tweaks::CleanInspect
5
+
6
+ # [MAGIC_NUMBER] [CHECKSUM] [METADATA_SIZE] [METADATA] [PAYLOAD]
7
+ MAGIC_NUMBER = [0x0e, 0x01].pack('C*').freeze
8
+ MAGIC_NUMBER_LEN = MAGIC_NUMBER.size
9
+ CHECKSUM_LEN = 4
10
+ METADATA_SIZE_LEN = 4
11
+
12
+ def initialize(buff)
13
+ @buff = buff
14
+ rewind
15
+ end
16
+
17
+ def decode
18
+ metadata = nil
19
+
20
+ message = PulsarSdk::Protocol::Message.new
21
+
22
+ mn_bytes = read_magic_number
23
+ if mn_bytes == MAGIC_NUMBER
24
+ _checksum = read_checksum
25
+ # TODO 可能需要校验一下,防止错误消息
26
+ metadata = read_metadata
27
+ else
28
+ rewind(MAGIC_NUMBER_LEN)
29
+ metadata = read_metadata
30
+ end
31
+
32
+ msg = read_remaining
33
+
34
+ # NOTE 同为Ruby SDK时可以根据Content-Type预先还原
35
+ # 复杂类型依旧为string,需要特别注意
36
+ metadata.properties.each do |x|
37
+ next unless x.key.to_s =~ /Content-Type/i
38
+ next unless x.value.to_s =~ /json/i
39
+ PulsarSdk.logger.info("#{self.class}::#{__method__}"){"Found json encode remark, parse JSON mesaage!"}
40
+ msg = JSON.parse(msg)
41
+ end
42
+
43
+ message.assign_attributes(
44
+ publish_time: metadata.publish_time,
45
+ event_time: metadata.event_time,
46
+ partition_key: metadata.partition_key,
47
+ properties: metadata.properties,
48
+ payload: msg
49
+ )
50
+
51
+ message
52
+ end
53
+
54
+ # 回退若干字节,方便处理非连续段
55
+ def rewind(x = nil)
56
+ return @readed = 0 if x.nil?
57
+
58
+ @readed -= x
59
+ end
60
+
61
+ def read_magic_number
62
+ read(MAGIC_NUMBER_LEN)
63
+ end
64
+
65
+ # crc32
66
+ def read_checksum
67
+ read(CHECKSUM_LEN)
68
+ end
69
+
70
+ def read_metadata
71
+ metadata_size = read(METADATA_SIZE_LEN, 'N')
72
+ metadata_bytes = read(metadata_size)
73
+ Pulsar::Proto::MessageMetadata.decode(metadata_bytes)
74
+ end
75
+
76
+ def read_remaining
77
+ payload_size = @buff.size - @readed
78
+ return if payload_size <= 0
79
+ read(payload_size)
80
+ end
81
+
82
+ private
83
+ def read(size, unpack = nil)
84
+ bytes = @buff[@readed..(@readed + size - 1)]
85
+ @readed += size
86
+
87
+ return bytes if unpack.nil? || bytes.nil?
88
+
89
+ bytes.unpack(unpack).first
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,74 @@
1
+ module PulsarSdk
2
+ module Protocol
3
+ class Topic
4
+ PUBLIC_TENANT = 'public'.freeze
5
+ DEFAULT_NAMESPACE = 'default'.freeze
6
+ PARTITIONED_TOPIC_SUFFIX = '-partition-'.freeze
7
+
8
+ prepend ::PulsarSdk::Tweaks::AssignAttributes
9
+
10
+ attr_accessor :domain, :namespace, :topic, :partition
11
+
12
+ def to_s
13
+ [
14
+ mk_domain,
15
+ self.namespace,
16
+ mk_topic
17
+ ].join('/')
18
+ end
19
+
20
+ private
21
+ def mk_topic
22
+ return self.topic if self.partition.nil?
23
+
24
+ "#{self.topic}#{PARTITIONED_TOPIC_SUFFIX}#{self.partition}"
25
+ end
26
+
27
+ def mk_domain
28
+ return if domain.nil?
29
+ "#{self.domain}:/"
30
+ end
31
+
32
+ # new: persistent://tenant/namespace/topic
33
+ # legacy: persistent://tenant/cluster/namespace/topic
34
+ def self.parse(topic)
35
+ if !topic.include?('://')
36
+ parts = topic.split('/')
37
+ if parts.size == 3 || parts.size == 4
38
+ topic = "persistent://#{topic}"
39
+ elsif parts.size == 1
40
+ topic = "persistent://#{PUBLIC_TENANT}/#{DEFAULT_NAMESPACE}/" + parts[0]
41
+ else
42
+ raise "Invalid short topic name: #{topic}, it should be in the format of <tenant>/<namespace>/<topic> or <topic>"
43
+ end
44
+ end
45
+
46
+ domain, rest = topic.split('://', 2)
47
+ unless ['persistent', 'non-persistent'].include?(domain)
48
+ raise "Invalid topic domain: #{domain}"
49
+ end
50
+
51
+ tn = new(domain: domain)
52
+ topic_with_partition = nil
53
+
54
+ case rest.count('/')
55
+ when 2
56
+ tenant, namespace, topic_with_partition = rest.split('/', 3)
57
+ tn.namespace = [tenant, namespace].join('/')
58
+ when 3
59
+ tenant, cluster, namespace, topic_with_partition = rest.split('/', 4)
60
+ tn.namespace = [tenant, cluster, namespace].join('/')
61
+ else
62
+ raise "Invalid topic name: #{topic}"
63
+ end
64
+
65
+ tn.topic, partition = topic_with_partition.split(PARTITIONED_TOPIC_SUFFIX, 2)
66
+
67
+ tn.partition = partition&.to_i
68
+
69
+ tn
70
+ end
71
+
72
+ end
73
+ end
74
+ end