mqtt-v5 0.0.1.ci.release

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mqtt/v5/async/client.rb +27 -0
  3. data/lib/mqtt/v5/client/authenticator.rb +83 -0
  4. data/lib/mqtt/v5/client/connection.rb +192 -0
  5. data/lib/mqtt/v5/client/session.rb +45 -0
  6. data/lib/mqtt/v5/client.rb +61 -0
  7. data/lib/mqtt/v5/errors.rb +19 -0
  8. data/lib/mqtt/v5/packet/auth.rb +57 -0
  9. data/lib/mqtt/v5/packet/connack.rb +110 -0
  10. data/lib/mqtt/v5/packet/connect.rb +130 -0
  11. data/lib/mqtt/v5/packet/disconnect.rb +78 -0
  12. data/lib/mqtt/v5/packet/ping_req.rb +20 -0
  13. data/lib/mqtt/v5/packet/ping_resp.rb +20 -0
  14. data/lib/mqtt/v5/packet/pub_ack.rb +60 -0
  15. data/lib/mqtt/v5/packet/pub_comp.rb +53 -0
  16. data/lib/mqtt/v5/packet/pub_rec.rb +60 -0
  17. data/lib/mqtt/v5/packet/pub_rel.rb +53 -0
  18. data/lib/mqtt/v5/packet/publish.rb +122 -0
  19. data/lib/mqtt/v5/packet/reason_code.rb +112 -0
  20. data/lib/mqtt/v5/packet/sub_ack.rb +66 -0
  21. data/lib/mqtt/v5/packet/subscribe.rb +87 -0
  22. data/lib/mqtt/v5/packet/unsub_ack.rb +59 -0
  23. data/lib/mqtt/v5/packet/unsubscribe.rb +112 -0
  24. data/lib/mqtt/v5/packet.rb +147 -0
  25. data/lib/mqtt/v5/packets.rb +25 -0
  26. data/lib/mqtt/v5/topic_alias/cache.rb +87 -0
  27. data/lib/mqtt/v5/topic_alias/frequency_weighted_policy.rb +50 -0
  28. data/lib/mqtt/v5/topic_alias/length_weighted_policy.rb +20 -0
  29. data/lib/mqtt/v5/topic_alias/lru_policy.rb +36 -0
  30. data/lib/mqtt/v5/topic_alias/manager.rb +143 -0
  31. data/lib/mqtt/v5/topic_alias/policy.rb +34 -0
  32. data/lib/mqtt/v5/topic_alias/weighted_policy.rb +48 -0
  33. data/lib/mqtt/v5/topic_alias.rb +27 -0
  34. data/lib/mqtt/v5/version.rb +11 -0
  35. data/lib/mqtt/v5.rb +4 -0
  36. metadata +88 -0
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'weighted_policy'
4
+
5
+ module MQTT
6
+ module V5
7
+ module TopicAlias
8
+ # Scores topics by `hits * bytesize. Tracks hit counts for all topics
9
+ class FrequencyWeightedPolicy
10
+ include WeightedPolicy
11
+
12
+ # The minimum topic size for a new topic to avoid O(n) scan of the topic list for each eviction
13
+ attr_reader :min_topic_score
14
+
15
+ def initialize
16
+ super
17
+ @hits = Hash.new(0)
18
+ end
19
+
20
+ # @!attribute [r] hits
21
+ # @return [Hash<String,Integer>] (frozen) topics by the number of times they have been published to.
22
+ def hits
23
+ @hits.dup.freeze
24
+ end
25
+
26
+ # Delete topics from the map (ie because they won't be used again) and recalculate the minimum topic score.
27
+ def clean!(*topics)
28
+ topics.each { |t| @hits.delete(t) }
29
+ end
30
+
31
+ # Evict the topic with the lowest score, unless the new_topic is even lower.
32
+ def evict(new_topic, &)
33
+ super.tap { |victim| @hits[new_topic] += 1 unless victim }
34
+ end
35
+
36
+ # Record a hit for topic, check if lower than minimum score.
37
+ def alias_hit(topic)
38
+ @hits[topic] += 1
39
+ super
40
+ end
41
+
42
+ private
43
+
44
+ def topic_score(topic, new: false)
45
+ (@hits[topic] + (new ? 1 : 0)) * topic.bytesize
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'weighted_policy'
4
+
5
+ module MQTT
6
+ module V5
7
+ module TopicAlias
8
+ # Minimum length policy - only aliases topics above a minimum size, evicts shortest
9
+ class LengthWeightedPolicy
10
+ include WeightedPolicy
11
+
12
+ private
13
+
14
+ def topic_score(topic, **)
15
+ topic.bytesize
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module V5
5
+ module TopicAlias
6
+ # Least Recently Used replacement policy
7
+ class LRUPolicy
8
+ def initialize
9
+ @topics = Set.new
10
+ end
11
+
12
+ # Alias any topic
13
+ def aliasable?(_packet)
14
+ true
15
+ end
16
+
17
+ # Evict the first entry in the Set (ie the least recently used)
18
+ def evict(_topic, &)
19
+ @topics.first
20
+ end
21
+
22
+ # Move the topic to the end of the Set (ie make it the most recently used)
23
+ def alias_hit(topic)
24
+ # Move the topic to the end of the set (relies on Set being ordered)
25
+ @topics.delete(topic)
26
+ @topics << topic
27
+ end
28
+
29
+ # Delete the evicted topic from the Set
30
+ def alias_evicted(topic)
31
+ @topics.delete(topic)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cache'
4
+ require_relative 'lru_policy'
5
+
6
+ module MQTT
7
+ module V5
8
+ module TopicAlias
9
+ # Manages topic aliases for a connection
10
+ class Manager
11
+ # maximum size of alias caches
12
+ MAXIMUM_ALIAS_ID = 65_535
13
+
14
+ # @return [Cache|nil] the incoming Cache
15
+ attr_reader :incoming
16
+
17
+ # @return [Cache|nil] the outgoing Cache
18
+ attr_reader :outgoing
19
+
20
+ # @return [Policy] the policy used to manage the outgoing Cache
21
+ attr_reader :policy
22
+
23
+ # Topic Alias Management
24
+ #
25
+ # Outgoing aliasing upper limit and replacement policy is configured by properties to {#initialize}
26
+ #
27
+ # For a Client...
28
+ # - Outgoing limit is further restricted by the CONNACK response from the Server.
29
+ # - Incoming limit is configured in the CONNECT packet. Records alias information as received from Server.
30
+ #
31
+ # For a Server...
32
+ # - Outgoing limit is further restricted by the CONNECT packet received from the Client.
33
+ # - Incoming limit is configured in the CONNACK packet. Records alias information as received from Client.
34
+ #
35
+ # @param [Integer|nil] send_maximum
36
+ # maximum number of topic aliases to hold for outgoing PUBLISH packets
37
+ #
38
+ # - `nil` will mean the value is determined only by the response from the other side
39
+ # - `0` (default) disables outgoing aliasing regardless of what the other side will accept
40
+ #
41
+ # @param [Policy|nil] policy replacement policy for evicting topics from the outgoing cache when
42
+ # it is full.
43
+ #
44
+ # - `nil` will use {LRUPolicy} as the default policy
45
+ #
46
+ # @return [Manager]
47
+ def initialize(send_maximum: 0, policy: nil)
48
+ @configured_outgoing_maximum = send_maximum || MAXIMUM_ALIAS_ID
49
+ @policy = policy
50
+ @policy ||= LRUPolicy.new if @configured_outgoing_maximum.positive?
51
+ end
52
+
53
+ # @visibility private
54
+ # Clear incoming alias cache
55
+ # - called from Client with CONNECT packet it will send to Server
56
+ # - called from Server with CONNACK packet it will send to Client
57
+ def clear_incoming!(packet)
58
+ @incoming = Cache.create(packet.topic_alias_maximum)
59
+ end
60
+
61
+ # @visibility private
62
+ # Clear outgoing alias cache
63
+ # - called from Client with CONNACK packet received from Server
64
+ # - called from Server with CONNECT packet received from Client
65
+ def clear_outgoing!(packet)
66
+ max = [@configured_outgoing_maximum, packet.topic_alias_maximum || 0].min
67
+ @outgoing = Cache.create(max)
68
+ end
69
+
70
+ # @visibility private
71
+ # Process incoming PUBLISH packet - resolve alias and update packet
72
+ # @param packet [Packet::Publish] incoming PUBLISH packet
73
+ # @raise [ProtocolError] if alias exceeds the maximum or cannot be resolved
74
+ def handle_incoming(packet)
75
+ return unless packet.topic_alias&.positive?
76
+
77
+ if (max = incoming&.max || 0) < packet.topic_alias
78
+ raise TopicAliasInvalid, "#{packet.topic_alias} exceeds maximum #{max}"
79
+ end
80
+
81
+ if packet.topic_name.empty?
82
+ resolved_topic = @incoming.resolve(packet.topic_alias)
83
+ raise TopicAliasInvalid, "Unknown topic alias #{packet.topic_alias}" unless resolved_topic
84
+
85
+ # override the empty topic name with the resolved topic
86
+ packet.apply_alias(name: resolved_topic)
87
+ else
88
+ @incoming.add(packet.topic_alias, packet.topic_name)
89
+ end
90
+ end
91
+
92
+ # Force removal of an outgoing topic from the alias cache. eg. because it is not going to be used any more.
93
+ # @param [Array<String>] topics the topics to remove
94
+ # @return [void]
95
+ # @note Prefer sending `topic_alias: false` to {Client#publish} to indicate topics that should not be aliased
96
+ def evict!(*topics)
97
+ topics.each do |topic|
98
+ @outgoing&.remove(topic) && @policy.alias_evicted(topic)
99
+ end
100
+ end
101
+
102
+ # @visibility private
103
+ # Get outgoing alias for topic (policy decides whether to alias and what to evict)
104
+ def handle_outgoing(packet)
105
+ return unless @outgoing && @policy&.aliasable?(packet)
106
+
107
+ if (alias_id = @outgoing.resolve(packet.topic_name))
108
+ return outgoing_alias_hit(alias_id, packet)
109
+ end
110
+
111
+ outgoing_alias_miss(packet)
112
+ end
113
+
114
+ private
115
+
116
+ def outgoing_alias_hit(alias_id, packet)
117
+ # We had one already, but we don't want to keep it any more (might as well use it now though)
118
+ evict!(packet.topic_name) unless packet.assign_alias?
119
+
120
+ @policy.alias_hit(packet.topic_name)
121
+ # We've seen and sent this one before, just send the alias_id and an empty topic name
122
+ packet.apply_alias(alias: alias_id, name: '')
123
+ end
124
+
125
+ def outgoing_alias_miss(packet)
126
+ return unless packet.assign_alias? # We don't want to alias this topic
127
+
128
+ alias_id = @outgoing.assign
129
+ unless alias_id
130
+ (evict = @policy.evict(packet.topic_name) { @outgoing.topics }) && evict!(evict)
131
+ alias_id = @outgoing.assign
132
+ end
133
+
134
+ return unless alias_id # evicting did not result in an available alias
135
+
136
+ @policy.alias_hit(packet.topic_name)
137
+ @outgoing.add(alias_id, packet.topic_name)
138
+ packet.apply_alias(alias: alias_id)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module V5
5
+ module TopicAlias
6
+ # Defaults the policy interface for evicting topics from a full Cache
7
+ # @abstract
8
+ module Policy
9
+ # @!method aliasable?(packet)
10
+ # @abstract
11
+ # @param [Packet::Publish] packet
12
+ # @return [Boolean] whether the packet is eligible for aliasing
13
+
14
+ # @method alias_evicted(topic)
15
+ # Called when an alias is evicted
16
+ # @param [String] topic The topic that was evicted.
17
+ # @return [void]
18
+
19
+ # @!method alias_hit(topic)
20
+ # Called when an alias is used (including the first time a topic is aliased)
21
+ # @param [String] topic
22
+ # @return [void]
23
+
24
+ # @!method evict(topic)
25
+ # Choose a topic to evict
26
+ # @param [String] topic The new topic that is looking for an alias
27
+ # @yield()
28
+ # @yieldreturn [Array<String>] the list of topics currently in the cache
29
+ # @return [String, nil] topic to evict
30
+ # @return [nil] evict nothing (ie do not alias the topic)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module V5
5
+ module TopicAlias
6
+ # Base module for evicting {Policy} based on a minimum topic score
7
+ module WeightedPolicy
8
+ def initialize
9
+ @min_topic_score = 0
10
+ end
11
+
12
+ # Aliases everything
13
+ def aliasable?(_packet)
14
+ true
15
+ end
16
+
17
+ # Evict the topic with the lowest score, unless the new_topic is even lower.
18
+ def evict(new_topic, &topics)
19
+ new_topic_score = topic_score(new_topic, new: true)
20
+
21
+ return nil unless new_topic_score >= @min_topic_score
22
+
23
+ victim = topics.call.min_by { |t| topic_score(t) }
24
+ @min_topic_score = topic_score(victim)
25
+ return victim if new_topic_score >= @min_topic_score
26
+
27
+ nil
28
+ end
29
+
30
+ # Record a hit for topic, check if lower than minimum score.
31
+ def alias_hit(topic)
32
+ @min_topic_score = [topic_score(topic), @min_topic_score].min
33
+ end
34
+
35
+ # Does nothing on eviction.
36
+ def alias_evicted(_topic)
37
+ # does not change scores
38
+ end
39
+
40
+ # @!method topic_score(topic, new: false)
41
+ # @abstract
42
+ # @param topic [String]
43
+ # @param new [Boolean] whether the topic is new (ie has not been aliased before)
44
+ # @return [Integer] the score of the topic
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'topic_alias/cache'
4
+ require_relative 'topic_alias/policy'
5
+ require_relative 'topic_alias/lru_policy'
6
+ require_relative 'topic_alias/frequency_weighted_policy'
7
+ require_relative 'topic_alias/length_weighted_policy'
8
+ require_relative 'topic_alias/manager'
9
+
10
+ module MQTT
11
+ module V5
12
+ # Topic Alias support for MQTT 5.0
13
+ #
14
+ # Provides bidirectional topic aliasing for Clients/Servers to reduce bandwidth usage by replacing
15
+ # repetitive topic names with small integer identifiers.
16
+ #
17
+ # @example Basic usage with default LRUPolicy policy
18
+ # alias_manager = MQTT::V5::TopicAlias::Manager.new(send_maximum: 100)
19
+ #
20
+ # @example Custom policy
21
+ # policy = MQTT::V5::TopicAlias::FrequencyWeightedPolicy.new
22
+ # alias_manager = MQTT::V5::TopicAlias::Manager.new(send_maximum: 50, policy: policy )
23
+ # @see Client#topic_aliases
24
+ module TopicAlias
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mqtt/version'
4
+
5
+ module MQTT
6
+ module V5
7
+ VERSION = MQTT::VERSION
8
+ MQTT_VERSION = Gem::Version.new('5.0')
9
+ PROTOCOL_VERSION = 0x05
10
+ end
11
+ end
data/lib/mqtt/v5.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'v5/version'
4
+ require_relative 'v5/client'
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mqtt-v5
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.ci.release
5
+ platform: ruby
6
+ authors:
7
+ - Grant Gardner
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mqtt-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: MQTT version 5.0 protocol implementation
27
+ email:
28
+ - grant@lastweekend.com.au
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/mqtt/v5.rb
34
+ - lib/mqtt/v5/async/client.rb
35
+ - lib/mqtt/v5/client.rb
36
+ - lib/mqtt/v5/client/authenticator.rb
37
+ - lib/mqtt/v5/client/connection.rb
38
+ - lib/mqtt/v5/client/session.rb
39
+ - lib/mqtt/v5/errors.rb
40
+ - lib/mqtt/v5/packet.rb
41
+ - lib/mqtt/v5/packet/auth.rb
42
+ - lib/mqtt/v5/packet/connack.rb
43
+ - lib/mqtt/v5/packet/connect.rb
44
+ - lib/mqtt/v5/packet/disconnect.rb
45
+ - lib/mqtt/v5/packet/ping_req.rb
46
+ - lib/mqtt/v5/packet/ping_resp.rb
47
+ - lib/mqtt/v5/packet/pub_ack.rb
48
+ - lib/mqtt/v5/packet/pub_comp.rb
49
+ - lib/mqtt/v5/packet/pub_rec.rb
50
+ - lib/mqtt/v5/packet/pub_rel.rb
51
+ - lib/mqtt/v5/packet/publish.rb
52
+ - lib/mqtt/v5/packet/reason_code.rb
53
+ - lib/mqtt/v5/packet/sub_ack.rb
54
+ - lib/mqtt/v5/packet/subscribe.rb
55
+ - lib/mqtt/v5/packet/unsub_ack.rb
56
+ - lib/mqtt/v5/packet/unsubscribe.rb
57
+ - lib/mqtt/v5/packets.rb
58
+ - lib/mqtt/v5/topic_alias.rb
59
+ - lib/mqtt/v5/topic_alias/cache.rb
60
+ - lib/mqtt/v5/topic_alias/frequency_weighted_policy.rb
61
+ - lib/mqtt/v5/topic_alias/length_weighted_policy.rb
62
+ - lib/mqtt/v5/topic_alias/lru_policy.rb
63
+ - lib/mqtt/v5/topic_alias/manager.rb
64
+ - lib/mqtt/v5/topic_alias/policy.rb
65
+ - lib/mqtt/v5/topic_alias/weighted_policy.rb
66
+ - lib/mqtt/v5/version.rb
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ rubygems_mfa_required: 'true'
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '3.4'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.6.9
86
+ specification_version: 4
87
+ summary: Ruby MQTT 5.0
88
+ test_files: []