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.
- checksums.yaml +7 -0
- data/.gitignore +51 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +107 -0
- data/Rakefile +2 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/protobuf/pulsar_api.pb.rb +710 -0
- data/lib/protobuf/pulsar_api.proto +934 -0
- data/lib/protobuf/validate.rb +41 -0
- data/lib/pulsar_admin.rb +14 -0
- data/lib/pulsar_admin/api.rb +215 -0
- data/lib/pulsar_sdk.rb +55 -0
- data/lib/pulsar_sdk/client.rb +13 -0
- data/lib/pulsar_sdk/client/connection.rb +371 -0
- data/lib/pulsar_sdk/client/connection_pool.rb +79 -0
- data/lib/pulsar_sdk/client/rpc.rb +67 -0
- data/lib/pulsar_sdk/consumer.rb +13 -0
- data/lib/pulsar_sdk/consumer/base.rb +148 -0
- data/lib/pulsar_sdk/consumer/manager.rb +127 -0
- data/lib/pulsar_sdk/consumer/message_tracker.rb +86 -0
- data/lib/pulsar_sdk/options.rb +6 -0
- data/lib/pulsar_sdk/options/base.rb +10 -0
- data/lib/pulsar_sdk/options/connection.rb +51 -0
- data/lib/pulsar_sdk/options/consumer.rb +34 -0
- data/lib/pulsar_sdk/options/producer.rb +14 -0
- data/lib/pulsar_sdk/options/reader.rb +7 -0
- data/lib/pulsar_sdk/options/tls.rb +8 -0
- data/lib/pulsar_sdk/producer.rb +14 -0
- data/lib/pulsar_sdk/producer/base.rb +154 -0
- data/lib/pulsar_sdk/producer/manager.rb +67 -0
- data/lib/pulsar_sdk/producer/message.rb +47 -0
- data/lib/pulsar_sdk/producer/router.rb +100 -0
- data/lib/pulsar_sdk/protocol.rb +8 -0
- data/lib/pulsar_sdk/protocol/frame.rb +53 -0
- data/lib/pulsar_sdk/protocol/lookup.rb +55 -0
- data/lib/pulsar_sdk/protocol/message.rb +55 -0
- data/lib/pulsar_sdk/protocol/namespace.rb +22 -0
- data/lib/pulsar_sdk/protocol/partitioned.rb +54 -0
- data/lib/pulsar_sdk/protocol/reader.rb +67 -0
- data/lib/pulsar_sdk/protocol/structure.rb +93 -0
- data/lib/pulsar_sdk/protocol/topic.rb +74 -0
- data/lib/pulsar_sdk/tweaks.rb +10 -0
- data/lib/pulsar_sdk/tweaks/assign_attributes.rb +30 -0
- data/lib/pulsar_sdk/tweaks/base_command.rb +66 -0
- data/lib/pulsar_sdk/tweaks/binary_heap.rb +133 -0
- data/lib/pulsar_sdk/tweaks/clean_inspect.rb +15 -0
- data/lib/pulsar_sdk/tweaks/time_at_microsecond.rb +27 -0
- data/lib/pulsar_sdk/tweaks/timeout_queue.rb +52 -0
- data/lib/pulsar_sdk/tweaks/wait_map.rb +81 -0
- data/lib/pulsar_sdk/version.rb +3 -0
- data/pulsar_sdk.gemspec +31 -0
- metadata +151 -0
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            require 'google/protobuf'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Google
         | 
| 4 | 
            +
              module Protobuf
         | 
| 5 | 
            +
                module MessageExts
         | 
| 6 | 
            +
                  alias_method :orig_to_json, :to_json
         | 
| 7 | 
            +
                  alias_method :orig_to_proto, :to_proto
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def to_json(options = {})
         | 
| 10 | 
            +
                    validate_presence!
         | 
| 11 | 
            +
                    orig_to_json(options)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def to_proto
         | 
| 15 | 
            +
                    validate_presence!
         | 
| 16 | 
            +
                    orig_to_proto
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def validate_presence!
         | 
| 20 | 
            +
                    self.class.descriptor.entries.each do |entry|
         | 
| 21 | 
            +
                      next if entry.label != :required
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      v = self[entry.name]
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                      validate_fail = case entry.type
         | 
| 26 | 
            +
                      when :int64, :uint64, :int32, :uint32,  :double, :float
         | 
| 27 | 
            +
                        v.nil? || v == 0
         | 
| 28 | 
            +
                      when :string
         | 
| 29 | 
            +
                        v.nil? || v.empty?
         | 
| 30 | 
            +
                      when :enum
         | 
| 31 | 
            +
                        v.nil? || !entry.subtype.entries.map(&:first).include?(v)
         | 
| 32 | 
            +
                      else
         | 
| 33 | 
            +
                        v.nil?
         | 
| 34 | 
            +
                      end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      raise "#{self.class.name}::#{entry.name} was required, but got 「#{v.inspect}」" if validate_fail
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
    
        data/lib/pulsar_admin.rb
    ADDED
    
    
| @@ -0,0 +1,215 @@ | |
| 1 | 
            +
            module PulsarAdmin
         | 
| 2 | 
            +
              class Api
         | 
| 3 | 
            +
                PublishTimeHeader = /^X-Pulsar-Publish-Time$/i
         | 
| 4 | 
            +
                BatchHeader       = /^X-Pulsar-Num-Batch-Message$/i
         | 
| 5 | 
            +
                PropertyPrefix    = /^X-Pulsar-PROPERTY-/i
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                # opts
         | 
| 8 | 
            +
                #   endpoint
         | 
| 9 | 
            +
                #   tenant
         | 
| 10 | 
            +
                #   persistent
         | 
| 11 | 
            +
                def initialize(opts)
         | 
| 12 | 
            +
                  @endpoint = URI.parse(opts[:endpoint])
         | 
| 13 | 
            +
                  @tenant = opts[:tenant]
         | 
| 14 | 
            +
                  @persistent = opts[:persistent] == false ? 'non-persistent' : 'persistent'
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def list_namespaces
         | 
| 18 | 
            +
                  get('/admin/v2/namespaces/:tenant').map do |ns|
         | 
| 19 | 
            +
                    ns.sub("#{@tenant}/", '')
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def create_namespace(name)
         | 
| 24 | 
            +
                  put('/admin/v2/namespaces/:tenant/:namespace', namespace: name)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def delete_namespace(name)
         | 
| 28 | 
            +
                  delete('/admin/v2/namespaces/:tenant/:namespace', namespace: name)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def namespace_topics(namespace)
         | 
| 32 | 
            +
                  result = {}
         | 
| 33 | 
            +
                  ['', '/partitioned'].flat_map do |pd|
         | 
| 34 | 
            +
                    resp = get("/admin/v2/:persistent/:tenant/:namespace#{pd}", namespace: namespace)
         | 
| 35 | 
            +
                    result[pd.empty? ? 'non-partitioned' : 'partitioned'] = resp
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  result
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def create_topic(namespace, topic, partitions = 0)
         | 
| 41 | 
            +
                  put("/admin/v2/:persistent/:tenant/:namespace/:topic#{partitions.zero? ? '' : '/partitions'}",
         | 
| 42 | 
            +
                        {namespace: namespace, topic: topic}, partitions
         | 
| 43 | 
            +
                      )
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def delete_topic(namespace, topic)
         | 
| 47 | 
            +
                  res1 = delete('/admin/v2/:persistent/:tenant/:namespace/:topic',
         | 
| 48 | 
            +
                          namespace: namespace, topic: topic
         | 
| 49 | 
            +
                        )
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  res2 = delete('/admin/v2/:persistent/:tenant/:namespace/:topic/partitions',
         | 
| 52 | 
            +
                    namespace: namespace, topic: topic
         | 
| 53 | 
            +
                  )
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  res1 || res2
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # options
         | 
| 59 | 
            +
                #   namespace
         | 
| 60 | 
            +
                #   topic
         | 
| 61 | 
            +
                #   sub_name
         | 
| 62 | 
            +
                #   message_position
         | 
| 63 | 
            +
                #   count
         | 
| 64 | 
            +
                def peek_messages(options)
         | 
| 65 | 
            +
                  (options[:count] || 1).times.map do |x|
         | 
| 66 | 
            +
                    opts = options.dup
         | 
| 67 | 
            +
                    opts[:message_position] = (opts[:message_position].to_i + x + 1).to_s
         | 
| 68 | 
            +
                    peek_message(opts)
         | 
| 69 | 
            +
                  end.compact
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                private
         | 
| 73 | 
            +
                def put(path, payload = {}, body = nil)
         | 
| 74 | 
            +
                  uri = @endpoint.dup
         | 
| 75 | 
            +
                  uri.path, payload = handle_restful_path(path, payload)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  req = Net::HTTP::Put.new(uri)
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  if payload.empty?
         | 
| 80 | 
            +
                    req.body = body.to_s
         | 
| 81 | 
            +
                    req.content_type = body.nil? ? 'application/json' : 'text/plain'
         | 
| 82 | 
            +
                  else
         | 
| 83 | 
            +
                    req.body = payload.to_json
         | 
| 84 | 
            +
                    req.content_type = 'application/json'
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  res = Net::HTTP.start(uri.hostname, uri.port) do |http|
         | 
| 88 | 
            +
                    http.request(req)
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  case res
         | 
| 92 | 
            +
                  when Net::HTTPSuccess, Net::HTTPNoContent
         | 
| 93 | 
            +
                    return true
         | 
| 94 | 
            +
                  else
         | 
| 95 | 
            +
                    puts "status: #{res.code} - body: #{res.body} - #{res.inspect}"
         | 
| 96 | 
            +
                    return false
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def raw_get(path, params = {})
         | 
| 101 | 
            +
                  uri = @endpoint.dup
         | 
| 102 | 
            +
                  uri.path, params = handle_restful_path(path, params)
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  req = Net::HTTP::Get.new(uri)
         | 
| 105 | 
            +
                  req.set_form_data(params) unless params.empty?
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  Net::HTTP.start(uri.hostname, uri.port) do |http|
         | 
| 108 | 
            +
                    http.request(req)
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                def get(path, params = {})
         | 
| 113 | 
            +
                  resp = raw_get(path, params)
         | 
| 114 | 
            +
                  try_decode_body(resp)
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def delete(path, payload = {})
         | 
| 118 | 
            +
                  uri = @endpoint.dup
         | 
| 119 | 
            +
                  uri.path, payload = handle_restful_path(path, payload)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  req = Net::HTTP::Delete.new(uri)
         | 
| 122 | 
            +
                  req.body = payload.to_json
         | 
| 123 | 
            +
                  req.content_type = 'application/json'
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  res = Net::HTTP.start(uri.hostname, uri.port) do |http|
         | 
| 126 | 
            +
                    http.request(req)
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  case res
         | 
| 130 | 
            +
                  when Net::HTTPSuccess, Net::HTTPNoContent
         | 
| 131 | 
            +
                    return true
         | 
| 132 | 
            +
                  else
         | 
| 133 | 
            +
                    return false
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                # options
         | 
| 138 | 
            +
                #   namespace
         | 
| 139 | 
            +
                #   topic
         | 
| 140 | 
            +
                #   sub_name
         | 
| 141 | 
            +
                #   message_position
         | 
| 142 | 
            +
                def peek_message(options)
         | 
| 143 | 
            +
                  options[:message_position] = options[:message_position].to_s
         | 
| 144 | 
            +
                  resp = raw_get('/admin/v2/:persistent/:tenant/:namespace/:topic/subscription/:sub_name/position/:message_position', options)
         | 
| 145 | 
            +
                  unless request_ok?(resp)
         | 
| 146 | 
            +
                    puts resp.body
         | 
| 147 | 
            +
                    return
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  payload = resp.body
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                  msg_id = nil
         | 
| 153 | 
            +
                  properties = {}
         | 
| 154 | 
            +
                  resp.each_header do |header|
         | 
| 155 | 
            +
                    case header
         | 
| 156 | 
            +
                    when PublishTimeHeader
         | 
| 157 | 
            +
                      properties['publish-time'] = resp.header[header]
         | 
| 158 | 
            +
                    when BatchHeader
         | 
| 159 | 
            +
                      properties['pulsar-num-batch-message'] = resp.header[header]
         | 
| 160 | 
            +
                    when PropertyPrefix
         | 
| 161 | 
            +
                      properties[header] = resp.header[header]
         | 
| 162 | 
            +
                    when /^X-Pulsar-Message-ID$/i
         | 
| 163 | 
            +
                      msg_id = resp.header[header]
         | 
| 164 | 
            +
                    end
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
                  [
         | 
| 167 | 
            +
                    msg_id,
         | 
| 168 | 
            +
                    properties,
         | 
| 169 | 
            +
                    payload
         | 
| 170 | 
            +
                  ]
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                def handle_restful_path(path, options)
         | 
| 174 | 
            +
                  return path unless path.include?(':')
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                  opts = combine_default_value(options || {})
         | 
| 177 | 
            +
                  opts.keys.sort.reverse.each do |k|
         | 
| 178 | 
            +
                    remark = ":#{k}"
         | 
| 179 | 
            +
                    next unless path.include?(remark)
         | 
| 180 | 
            +
                    path.gsub!(remark, opts[k])
         | 
| 181 | 
            +
                    options.delete(k)
         | 
| 182 | 
            +
                  end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                  return [path, options]
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                def combine_default_value(opts)
         | 
| 188 | 
            +
                  opts.merge(
         | 
| 189 | 
            +
                    tenant: @tenant,
         | 
| 190 | 
            +
                    persistent: @persistent
         | 
| 191 | 
            +
                  )
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                def request_ok?(resp)
         | 
| 195 | 
            +
                  case resp
         | 
| 196 | 
            +
                  when Net::HTTPSuccess
         | 
| 197 | 
            +
                    true
         | 
| 198 | 
            +
                  when Net::HTTPRedirection
         | 
| 199 | 
            +
                    false
         | 
| 200 | 
            +
                  else
         | 
| 201 | 
            +
                    false
         | 
| 202 | 
            +
                  end
         | 
| 203 | 
            +
                end
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                def try_decode_body(resp)
         | 
| 206 | 
            +
                  unless request_ok?(resp)
         | 
| 207 | 
            +
                    puts resp.body
         | 
| 208 | 
            +
                    return
         | 
| 209 | 
            +
                  end
         | 
| 210 | 
            +
                  return resp.body unless resp.content_type =~ /application\/json/
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                  JSON.parse(resp.body) rescue resp.body
         | 
| 213 | 
            +
                end
         | 
| 214 | 
            +
              end
         | 
| 215 | 
            +
            end
         | 
    
        data/lib/pulsar_sdk.rb
    ADDED
    
    | @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            require 'logger'
         | 
| 2 | 
            +
            require 'json'
         | 
| 3 | 
            +
            require 'uri'
         | 
| 4 | 
            +
            require 'protobuf/validate'
         | 
| 5 | 
            +
            require 'protobuf/pulsar_api.pb'
         | 
| 6 | 
            +
            require "pulsar_sdk/version"
         | 
| 7 | 
            +
            require 'pulsar_sdk/tweaks'
         | 
| 8 | 
            +
            require 'pulsar_sdk/protocol'
         | 
| 9 | 
            +
            require 'pulsar_sdk/options'
         | 
| 10 | 
            +
            require 'pulsar_sdk/consumer'
         | 
| 11 | 
            +
            require 'pulsar_sdk/producer'
         | 
| 12 | 
            +
            require 'pulsar_sdk/client'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            module PulsarSdk
         | 
| 15 | 
            +
              extend self
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              # options Hash see PulsarSdk::Options::Connection for detail
         | 
| 18 | 
            +
              def create_client(options)
         | 
| 19 | 
            +
                opts = ::PulsarSdk::Options::Connection.new(options)
         | 
| 20 | 
            +
                ::PulsarSdk::Client.create(opts)
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              # options Hash see PulsarSdk::Options::Producer for detail
         | 
| 24 | 
            +
              def create_producer(client, options)
         | 
| 25 | 
            +
                opts = ::PulsarSdk::Options::Producer.new(options)
         | 
| 26 | 
            +
                client.create_producer(opts)
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              # options Hash see PulsarSdk::Options::Consumer for detail
         | 
| 30 | 
            +
              def create_consumer(client, options)
         | 
| 31 | 
            +
                opts = ::PulsarSdk::Options::Consumer.new(options)
         | 
| 32 | 
            +
                client.subscribe(opts)
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def logger
         | 
| 36 | 
            +
                @logger ||= Logger.new(STDOUT).tap do |logger|
         | 
| 37 | 
            +
                              logger.formatter = Formatter.new
         | 
| 38 | 
            +
                            end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def logger=(v)
         | 
| 42 | 
            +
                @logger = v
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              class Formatter < ::Logger::Formatter
         | 
| 46 | 
            +
                def call(severity, timestamp, progname, msg)
         | 
| 47 | 
            +
                  case msg
         | 
| 48 | 
            +
                  when ::StandardError
         | 
| 49 | 
            +
                    msg = [msg.message, msg&.backtrace].join(":\n")
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  super
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,371 @@ | |
| 1 | 
            +
            require 'socket'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module PulsarSdk
         | 
| 4 | 
            +
              module Client
         | 
| 5 | 
            +
                class Connection
         | 
| 6 | 
            +
                  prepend ::PulsarSdk::Tweaks::CleanInspect
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  CLIENT_NAME = "pulsar-client-#{PulsarSdk::VERSION}".freeze
         | 
| 9 | 
            +
                  PROTOCOL_VER = Pulsar::Proto::ProtocolVersion::V13
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  attr_reader :consumer_handlers
         | 
| 12 | 
            +
                  attr_reader :producer_handlers
         | 
| 13 | 
            +
                  attr_reader :response_container # 用于处理状态回调
         | 
| 14 | 
            +
                  attr_reader :seq_generator
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # opts PulsarSdk::Options::Connection
         | 
| 17 | 
            +
                  def initialize(opts)
         | 
| 18 | 
            +
                    @conn_options = opts
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    @socket = nil
         | 
| 21 | 
            +
                    @state = Status.new
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    @seq_generator = SeqGenerator.new
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    @consumer_handlers = ConsumerHandler.new
         | 
| 26 | 
            +
                    @producer_handlers = ProducerHandler.new
         | 
| 27 | 
            +
                    @response_container = ResponseContainer.new
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def start
         | 
| 31 | 
            +
                    unless connect && do_hand_shake && listen
         | 
| 32 | 
            +
                      @state.closed!
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def self.establish(opts)
         | 
| 37 | 
            +
                    conn = new(opts).tap do |c|
         | 
| 38 | 
            +
                      c.start
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                    # TODO check connection ready
         | 
| 41 | 
            +
                    conn
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def close
         | 
| 45 | 
            +
                    @state.closed!
         | 
| 46 | 
            +
                    consumer_handlers.each{|_k, v| v.call}
         | 
| 47 | 
            +
                    producer_handlers.each{|_k, v| v.call}
         | 
| 48 | 
            +
                    Timeout::timeout(2) {@pong&.join} rescue @pong&.kill
         | 
| 49 | 
            +
                    @pong&.join
         | 
| 50 | 
            +
                  ensure
         | 
| 51 | 
            +
                    @socket.close
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def closed?
         | 
| 55 | 
            +
                    @state.closed?
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def ping
         | 
| 59 | 
            +
                    base_cmd = Pulsar::Proto::BaseCommand.new(
         | 
| 60 | 
            +
                      type: Pulsar::Proto::BaseCommand::Type::PING,
         | 
| 61 | 
            +
                      ping: Pulsar::Proto::CommandPing.new
         | 
| 62 | 
            +
                    )
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    request(base_cmd, nil, true)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    @state.ping!
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def active_status
         | 
| 70 | 
            +
                    [@state.last_ping_at, @state.last_received_at]
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def request(cmd, msg = nil, async = false, timeout = nil)
         | 
| 74 | 
            +
                    raise 'connection was closed!' if closed?
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    cmd.seq_generator ||= @seq_generator
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    # NOTE try to auto set *_id
         | 
| 79 | 
            +
                    cmd.handle_ids
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    frame = PulsarSdk::Protocol::Frame.encode(cmd, msg)
         | 
| 82 | 
            +
                    write(frame)
         | 
| 83 | 
            +
                    return true if async
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    if request_id = cmd.get_request_id
         | 
| 86 | 
            +
                      return @response_container.delete(request_id, timeout)
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    true
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  private
         | 
| 93 | 
            +
                  def reader
         | 
| 94 | 
            +
                    @reader ||= PulsarSdk::Protocol::Reader.new(@socket)
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  def write(bytes)
         | 
| 98 | 
            +
                    begin
         | 
| 99 | 
            +
                      @socket.write_nonblock(bytes)
         | 
| 100 | 
            +
                    rescue IO::WaitWritable
         | 
| 101 | 
            +
                      IO.select(nil, [@socket], nil, @conn_options.operation_timeout)
         | 
| 102 | 
            +
                      retry
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  def listen
         | 
| 107 | 
            +
                    @pong = Thread.new do
         | 
| 108 | 
            +
                      loop do
         | 
| 109 | 
            +
                        break if closed?
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                        begin
         | 
| 112 | 
            +
                          @state.ready? ? read_from_connection : @state.wait
         | 
| 113 | 
            +
                        rescue Errno::ETIMEDOUT
         | 
| 114 | 
            +
                          # read timeout, do nothing
         | 
| 115 | 
            +
                        rescue => e
         | 
| 116 | 
            +
                          PulsarSdk.logger.error("reader error") {e}
         | 
| 117 | 
            +
                          close
         | 
| 118 | 
            +
                        end
         | 
| 119 | 
            +
                      end
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                    @pong.abort_on_exception = false
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    true
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  def connect
         | 
| 127 | 
            +
                    return true if (@socket && !closed?)
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
         | 
| 130 | 
            +
                    @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
         | 
| 131 | 
            +
                    @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                    host_port = @conn_options.port_and_host_from(:logical_addr)
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                    sockaddr = Socket.sockaddr_in(*host_port)
         | 
| 136 | 
            +
                    begin
         | 
| 137 | 
            +
                      # Initiate the socket connection in the background. If it doesn't fail
         | 
| 138 | 
            +
                      # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
         | 
| 139 | 
            +
                      # indicating the connection is in progress.
         | 
| 140 | 
            +
                      @socket.connect_nonblock(sockaddr)
         | 
| 141 | 
            +
                    rescue IO::WaitWritable
         | 
| 142 | 
            +
                      # IO.select will block until the socket is writable or the timeout
         | 
| 143 | 
            +
                      # is exceeded, whichever comes first.
         | 
| 144 | 
            +
                      unless IO.select(nil, [@socket], nil, @conn_options.connection_timeout)
         | 
| 145 | 
            +
                        # IO.select returns nil when the socket is not ready before timeout
         | 
| 146 | 
            +
                        # seconds have elapsed
         | 
| 147 | 
            +
                        @socket.close
         | 
| 148 | 
            +
                        return false
         | 
| 149 | 
            +
                      end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                      begin
         | 
| 152 | 
            +
                        # Verify there is now a good connection.
         | 
| 153 | 
            +
                        @socket.connect_nonblock(sockaddr)
         | 
| 154 | 
            +
                      rescue Errno::EISCONN
         | 
| 155 | 
            +
                        # The socket is connected, we're good!
         | 
| 156 | 
            +
                      end
         | 
| 157 | 
            +
                    end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                    @state.tcp_connected!
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                    true
         | 
| 162 | 
            +
                  end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                  def do_hand_shake
         | 
| 165 | 
            +
                    base_cmd = Pulsar::Proto::BaseCommand.new(
         | 
| 166 | 
            +
                      type: Pulsar::Proto::BaseCommand::Type::CONNECT,
         | 
| 167 | 
            +
                      connect: Pulsar::Proto::CommandConnect.new(
         | 
| 168 | 
            +
                        client_version: CLIENT_NAME,
         | 
| 169 | 
            +
                        protocol_version: PROTOCOL_VER,
         | 
| 170 | 
            +
                        proxy_to_broker_url: @conn_options.proxy_to_broker_url
         | 
| 171 | 
            +
                      )
         | 
| 172 | 
            +
                    )
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                    request(base_cmd)
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                    @state.ready!
         | 
| 177 | 
            +
                    true
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  def read_from_connection
         | 
| 181 | 
            +
                    base_cmd, meta_and_payload = reader.read_fully
         | 
| 182 | 
            +
                    return if base_cmd.nil?
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                    @state.received!
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                    handle_base_command(base_cmd, meta_and_payload)
         | 
| 187 | 
            +
                  end
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  def handle_base_command(cmd, payload)
         | 
| 190 | 
            +
                    PulsarSdk.logger.debug(__method__){cmd.type} unless cmd.typeof_ping?
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                    case
         | 
| 193 | 
            +
                    when cmd.typeof_success?
         | 
| 194 | 
            +
                      handle_response(cmd)
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                    when cmd.typeof_connected?
         | 
| 197 | 
            +
                      PulsarSdk.logger.info(__method__){"#{cmd.type}: #{cmd.connected}"}
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                    when cmd.typeof_producer_success?
         | 
| 200 | 
            +
                      handle_response(cmd)
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    when cmd.typeof_lookup_response?
         | 
| 203 | 
            +
                      handle_response(cmd)
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                    when cmd.typeof_get_last_message_id_response?
         | 
| 206 | 
            +
                      handle_response(cmd)
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                    when cmd.typeof_consumer_stats_response?
         | 
| 209 | 
            +
                      handle_response(cmd)
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                    when cmd.typeof_reached_end_of_topic?
         | 
| 212 | 
            +
                      # TODO notify consumer no more message
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                    when cmd.typeof_get_topics_of_namespace_response?
         | 
| 215 | 
            +
                      handle_response(cmd)
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                    when cmd.typeof_get_schema_response?
         | 
| 218 | 
            +
                    when cmd.typeof_partitioned_metadata_response?
         | 
| 219 | 
            +
                      handle_response(cmd)
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                    when cmd.typeof_error?
         | 
| 222 | 
            +
                      PulsarSdk.logger.error(__method__){"#{cmd.error}: #{cmd.message}"}
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                    when cmd.typeof_close_producer?
         | 
| 225 | 
            +
                      producer_id = cmd.close_producer.producer_id
         | 
| 226 | 
            +
                      producer_handlers.find(producer_id)&.call
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                    when cmd.typeof_close_consumer?
         | 
| 229 | 
            +
                      consumer_id = cmd.close_consumer.consumer_id
         | 
| 230 | 
            +
                      consumer_handlers.find(consumer_id)&.call
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                    when cmd.typeof_active_consumer_change?
         | 
| 233 | 
            +
                    when cmd.typeof_message?
         | 
| 234 | 
            +
                      handle_message(cmd, payload)
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                    when cmd.typeof_send_receipt?
         | 
| 237 | 
            +
                      handle_send_receipt(cmd)
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                    when cmd.typeof_ping?
         | 
| 240 | 
            +
                      handle_ping
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                    when cmd.typeof_pong?
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                    else
         | 
| 245 | 
            +
                      close
         | 
| 246 | 
            +
                      raise "Received invalid command type: #{cmd.type}"
         | 
| 247 | 
            +
                    end
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                    true
         | 
| 250 | 
            +
                  end
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                  def handle_response(cmd)
         | 
| 253 | 
            +
                    request_id = cmd.get_request_id
         | 
| 254 | 
            +
                    return if request_id.nil?
         | 
| 255 | 
            +
                    @response_container.add(request_id, cmd)
         | 
| 256 | 
            +
                  end
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                  def handle_message(cmd, payload)
         | 
| 259 | 
            +
                    consumer_id = cmd.get_consumer_id
         | 
| 260 | 
            +
                    if consumer_id.nil?
         | 
| 261 | 
            +
                      ::PulsarSdk.logger.warn(__method__){"can not get consumer id from cmd: #{cmd.inspect}"}
         | 
| 262 | 
            +
                      return
         | 
| 263 | 
            +
                    end
         | 
| 264 | 
            +
                    handler = consumer_handlers.find(consumer_id)
         | 
| 265 | 
            +
                    if handler.nil?
         | 
| 266 | 
            +
                      ::PulsarSdk.logger.warn(__method__){"can not get consumer_handler from cmd: #{cmd.inspect}"}
         | 
| 267 | 
            +
                      return
         | 
| 268 | 
            +
                    end
         | 
| 269 | 
            +
                    handler.call(cmd, payload)
         | 
| 270 | 
            +
                  end
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                  def handle_send_receipt(cmd)
         | 
| 273 | 
            +
                    send_receipt = cmd.send_receipt
         | 
| 274 | 
            +
                    producer_id = send_receipt.producer_id
         | 
| 275 | 
            +
                    handler = producer_handlers.find(producer_id)
         | 
| 276 | 
            +
                    return if handler.nil?
         | 
| 277 | 
            +
                    handler.call(send_receipt)
         | 
| 278 | 
            +
                  end
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                  def handle_ping
         | 
| 281 | 
            +
                    base_cmd = Pulsar::Proto::BaseCommand.new(
         | 
| 282 | 
            +
                      type: Pulsar::Proto::BaseCommand::Type::PONG,
         | 
| 283 | 
            +
                      pong: Pulsar::Proto::CommandPong.new
         | 
| 284 | 
            +
                    )
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                    request(base_cmd, nil, true)
         | 
| 287 | 
            +
                  end
         | 
| 288 | 
            +
             | 
| 289 | 
            +
                  class Status
         | 
| 290 | 
            +
                    attr_reader :last_received_at, :last_ping_at
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                    STATUS = %w[
         | 
| 293 | 
            +
                      init
         | 
| 294 | 
            +
                      connecting
         | 
| 295 | 
            +
                      tcp_connected
         | 
| 296 | 
            +
                      ready
         | 
| 297 | 
            +
                      closed
         | 
| 298 | 
            +
                    ].freeze
         | 
| 299 | 
            +
             | 
| 300 | 
            +
                    def initialize
         | 
| 301 | 
            +
                      @state = 'init'
         | 
| 302 | 
            +
                      @lock = Mutex.new
         | 
| 303 | 
            +
                      @signal = ConditionVariable.new
         | 
| 304 | 
            +
                      @last_received_at = 0
         | 
| 305 | 
            +
                      @last_ping_at = 0
         | 
| 306 | 
            +
                    end
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                    def wait
         | 
| 309 | 
            +
                      @lock.synchronize do
         | 
| 310 | 
            +
                        @signal.wait(@lock)
         | 
| 311 | 
            +
                      end
         | 
| 312 | 
            +
                    end
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                    def received!
         | 
| 315 | 
            +
                      @lock.synchronize do
         | 
| 316 | 
            +
                        @last_received_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
         | 
| 317 | 
            +
                      end
         | 
| 318 | 
            +
                    end
         | 
| 319 | 
            +
             | 
| 320 | 
            +
                    def ping!
         | 
| 321 | 
            +
                      @lock.synchronize do
         | 
| 322 | 
            +
                        @last_ping_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
         | 
| 323 | 
            +
                      end
         | 
| 324 | 
            +
                    end
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                    STATUS.each do |x|
         | 
| 327 | 
            +
                      define_method "#{x.to_s.downcase}?" do
         | 
| 328 | 
            +
                        @state == x
         | 
| 329 | 
            +
                      end
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                      define_method "#{x.to_s.downcase}!" do
         | 
| 332 | 
            +
                        @lock.synchronize do
         | 
| 333 | 
            +
                          @state = x
         | 
| 334 | 
            +
                          @signal.broadcast
         | 
| 335 | 
            +
                        end
         | 
| 336 | 
            +
                      end
         | 
| 337 | 
            +
                    end
         | 
| 338 | 
            +
                  end
         | 
| 339 | 
            +
             | 
| 340 | 
            +
                  class SeqGenerator
         | 
| 341 | 
            +
                    def initialize
         | 
| 342 | 
            +
                      @mutex = Mutex.new
         | 
| 343 | 
            +
                      @seq = {}
         | 
| 344 | 
            +
                    end
         | 
| 345 | 
            +
             | 
| 346 | 
            +
                    # def new_request_id
         | 
| 347 | 
            +
                    # def new_producer_id
         | 
| 348 | 
            +
                    # def new_consumer_id
         | 
| 349 | 
            +
                    # def new_sequence_id
         | 
| 350 | 
            +
                    [:request_id, :producer_id, :consumer_id, :sequence_id].each do |k|
         | 
| 351 | 
            +
                      define_method "new_#{k}" do
         | 
| 352 | 
            +
                        next!(k)
         | 
| 353 | 
            +
                      end
         | 
| 354 | 
            +
                    end
         | 
| 355 | 
            +
             | 
| 356 | 
            +
                    def next!(key)
         | 
| 357 | 
            +
                      @mutex.synchronize do
         | 
| 358 | 
            +
                        @seq[key] ||= 0
         | 
| 359 | 
            +
                        @seq[key] += 1
         | 
| 360 | 
            +
                      end
         | 
| 361 | 
            +
                    end
         | 
| 362 | 
            +
                  end
         | 
| 363 | 
            +
             | 
| 364 | 
            +
                  class ConsumerHandler < ::PulsarSdk::Tweaks::WaitMap; end
         | 
| 365 | 
            +
             | 
| 366 | 
            +
                  class ProducerHandler < ::PulsarSdk::Tweaks::WaitMap; end
         | 
| 367 | 
            +
             | 
| 368 | 
            +
                  class ResponseContainer < ::PulsarSdk::Tweaks::WaitMap; end
         | 
| 369 | 
            +
                end
         | 
| 370 | 
            +
              end
         | 
| 371 | 
            +
            end
         |