mqkv 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52ec1386119f7a8371f41f5942360c248f6b9e9c3a4efaf67a80fac35d1a2d27
4
+ data.tar.gz: 1863f574c1fa148399abb666248cec1e2f31b2e9b5c4da52b686e9c2ce84b7bf
5
+ SHA512:
6
+ metadata.gz: 761d510fea8ee47f91116b7c11f61435f47095cd92d8805603065d5d19723d3027111ed1ee89c4c5785ce2fb9c70cb8766fe55508f5cf3b8d48f563ea5d0021d
7
+ data.tar.gz: 877451290430c1b5e268881c6193f239619a0f9c4c506b3f6697c70939404f4ef8fea80561e4dc634f48d81c0167f6ba85680ca40fba748ea06d38763f46996c
data/lib/mqkv/store.rb ADDED
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "amqp-client"
4
+ require "set"
5
+
6
+ module MQKV
7
+ class Store
8
+ WatchHandle = Data.define(:channel, :consumer_tag)
9
+
10
+ def initialize(url, prefix: "mqkv", read_timeout: 0.5)
11
+ @url = url
12
+ @prefix = prefix
13
+ @read_timeout = read_timeout
14
+ @mutex = Mutex.new
15
+ @connection = nil
16
+ @declared_streams = Set.new
17
+ end
18
+
19
+ def set(key, value)
20
+ name = queue_name(key)
21
+ ensure_stream(name)
22
+ connection.with_channel do |ch|
23
+ ch.basic_publish_confirm(value.to_s, exchange: "", routing_key: name)
24
+ end
25
+ nil
26
+ end
27
+
28
+ def get(key)
29
+ messages = consume_stream(queue_name(key), offset: "last")
30
+ return nil if messages.empty?
31
+
32
+ last = messages.last
33
+ return nil if tombstone?(last)
34
+
35
+ last.body
36
+ end
37
+
38
+ def delete(key)
39
+ name = queue_name(key)
40
+ ensure_stream(name)
41
+ connection.with_channel do |ch|
42
+ ch.basic_publish_confirm("", exchange: "", routing_key: name,
43
+ headers: { "__mqkv_deleted__" => true })
44
+ end
45
+ nil
46
+ end
47
+
48
+ def exists?(key)
49
+ !get(key).nil?
50
+ end
51
+
52
+ def history(key, limit: 10)
53
+ messages = consume_stream(queue_name(key), offset: "first")
54
+ values = []
55
+ messages.each do |msg|
56
+ if tombstone?(msg)
57
+ values.clear
58
+ else
59
+ values << msg.body
60
+ end
61
+ end
62
+ values.last(limit)
63
+ end
64
+
65
+ def watch(key, &block)
66
+ name = queue_name(key)
67
+ ensure_stream(name)
68
+ ch = connection.channel
69
+ ch.basic_qos(256)
70
+ consume_ok = ch.basic_consume(name, no_ack: false,
71
+ arguments: { "x-stream-offset" => "next" },
72
+ worker_threads: 1) do |msg|
73
+ msg.ack
74
+ block.call(msg.body) unless tombstone?(msg)
75
+ end
76
+ WatchHandle.new(channel: ch, consumer_tag: consume_ok.consumer_tag)
77
+ end
78
+
79
+ def unwatch(handle)
80
+ handle.channel.basic_cancel(handle.consumer_tag)
81
+ handle.channel.close
82
+ end
83
+
84
+ def purge!
85
+ conn = @mutex.synchronize { @connection }
86
+ return unless conn && !conn.closed?
87
+
88
+ streams = @mutex.synchronize { @declared_streams.to_a }
89
+ conn.with_channel do |ch|
90
+ streams.each { |name| ch.queue_delete(name) }
91
+ end
92
+ @mutex.synchronize { @declared_streams.clear }
93
+ end
94
+
95
+ def close
96
+ @mutex.synchronize do
97
+ @connection&.close
98
+ @connection = nil
99
+ @declared_streams.clear
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def connection
106
+ @mutex.synchronize do
107
+ return @connection if @connection && !@connection.closed?
108
+
109
+ @connection = AMQP::Client.new(@url).connect
110
+ @declared_streams.clear
111
+ @connection
112
+ end
113
+ end
114
+
115
+ def queue_name(key)
116
+ "#{@prefix}.#{key}"
117
+ end
118
+
119
+ def ensure_stream(name)
120
+ already_declared = @mutex.synchronize { @declared_streams.include?(name) }
121
+ return if already_declared
122
+
123
+ connection.with_channel do |ch|
124
+ ch.queue_declare(name, durable: true, arguments: { "x-queue-type" => "stream" })
125
+ end
126
+ @mutex.synchronize { @declared_streams.add(name) }
127
+ end
128
+
129
+ def consume_stream(name, offset:)
130
+ ensure_stream(name)
131
+ ch = connection.channel
132
+ begin
133
+ ch.basic_qos(256)
134
+ collected = []
135
+ q = ::Queue.new
136
+
137
+ consume_ok = ch.basic_consume(name, no_ack: false,
138
+ arguments: { "x-stream-offset" => offset },
139
+ worker_threads: 1) do |msg|
140
+ q.push(msg)
141
+ end
142
+
143
+ loop do
144
+ msg = q.pop(timeout: @read_timeout)
145
+ break if msg.nil?
146
+
147
+ collected << msg
148
+ end
149
+
150
+ ch.basic_ack(collected.last.delivery_tag, multiple: true) if collected.any?
151
+ ch.basic_cancel(consume_ok.consumer_tag)
152
+ collected
153
+ ensure
154
+ ch.close
155
+ end
156
+ end
157
+
158
+ def tombstone?(msg)
159
+ msg.properties&.headers&.fetch("__mqkv_deleted__", false) == true
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQKV
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mqkv.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mqkv/version"
4
+ require_relative "mqkv/store"
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mqkv
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mqkv contributors
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: amqp-client
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: Uses AMQP stream queues (RabbitMQ 3.9+ / LavinMQ) as a key-value store.
27
+ Each key maps to a dedicated stream queue; the latest message is the current value.
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - lib/mqkv.rb
33
+ - lib/mqkv/store.rb
34
+ - lib/mqkv/version.rb
35
+ homepage: https://github.com/dentarg/mqkv
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.3'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 4.0.7
54
+ specification_version: 4
55
+ summary: Key-value store backed by AMQP stream queues
56
+ test_files: []