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,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