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 +7 -0
- data/lib/mqkv/store.rb +162 -0
- data/lib/mqkv/version.rb +5 -0
- data/lib/mqkv.rb +4 -0
- metadata +56 -0
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
|
data/lib/mqkv/version.rb
ADDED
data/lib/mqkv.rb
ADDED
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: []
|