redis_exchange 0.3.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/README.md +542 -0
- data/docs/architecture.md +326 -0
- data/lib/redis_exchange/depth_stream_consumer.rb +39 -0
- data/lib/redis_exchange/ledger_stream_consumer.rb +14 -0
- data/lib/redis_exchange/maintenance.rb +641 -0
- data/lib/redis_exchange/order_stream_consumer.rb +17 -0
- data/lib/redis_exchange/stream_consumer.rb +163 -0
- data/lib/redis_exchange/trade_stream_consumer.rb +17 -0
- data/lib/redis_exchange/version.rb +5 -0
- data/lib/redis_exchange.rb +1544 -0
- data/redis_scripts/audit_user_balance.lua +150 -0
- data/redis_scripts/balance_op.lua +148 -0
- data/redis_scripts/cancel_order.lua +277 -0
- data/redis_scripts/finalize_precision_update.lua +88 -0
- data/redis_scripts/force_cancel_order.lua +222 -0
- data/redis_scripts/match_order.lua +1026 -0
- data/redis_scripts/migrate_orders_batch.lua +204 -0
- data/redis_scripts/update_market_precision.lua +260 -0
- data/script/export_rdb_data.sh +234 -0
- data/script/export_snapshot.rb +370 -0
- data/script/maintenance/adjust_precision.rb +176 -0
- data/script/rename_asset.rb +314 -0
- data/script/rename_market.rb +285 -0
- metadata +149 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RedisExchange
|
|
6
|
+
# StreamConsumer
|
|
7
|
+
# --------------
|
|
8
|
+
# Base class for Redis Streams consumer-group operations.
|
|
9
|
+
# Provides common logic for ensure_group, read, ack, pending, and claim.
|
|
10
|
+
class StreamConsumer
|
|
11
|
+
Entry = Struct.new(:stream, :id, :fields)
|
|
12
|
+
|
|
13
|
+
attr_reader :redis, :stream_key, :group, :consumer
|
|
14
|
+
|
|
15
|
+
def initialize(redis:, group:, consumer:, stream_key:)
|
|
16
|
+
@redis = redis
|
|
17
|
+
@group = group
|
|
18
|
+
@consumer = consumer
|
|
19
|
+
@stream_key = stream_key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Ensure the consumer group exists; create starting at `start_id`.
|
|
23
|
+
# Use '0' to consume existing backlog; use '$' to start from the end.
|
|
24
|
+
def ensure_group(start_id: '0')
|
|
25
|
+
begin
|
|
26
|
+
@redis.xgroup(:create, stream_key, group, start_id, mkstream: true)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
# Ignore group exists errors across redis-rb versions
|
|
29
|
+
msg = e.message.to_s.downcase
|
|
30
|
+
return if msg.include?('busygroup') || msg.include?('exists')
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Read a batch of entries as Entry objects.
|
|
37
|
+
# Returns [] on timeout or when no entries are available.
|
|
38
|
+
# @param id [String] '>' for new entries, '0' for pending entries
|
|
39
|
+
def read(count: 100, block_ms: 2000, id: '>')
|
|
40
|
+
# Use positional args form to support older redis-rb versions
|
|
41
|
+
raw = @redis.xreadgroup(group, consumer, stream_key, id, count: count, block: block_ms)
|
|
42
|
+
normalize_read_result(raw)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Convenience loop: continuously read and yield entries to the given block.
|
|
46
|
+
# Stops when the block returns :halt. If auto_ack is true, ack after block
|
|
47
|
+
# executes successfully (no exception raised).
|
|
48
|
+
def each(count: 100, block_ms: 2000, auto_ack: false)
|
|
49
|
+
raise ArgumentError, 'block required' unless block_given?
|
|
50
|
+
loop do
|
|
51
|
+
entries = read(count: count, block_ms: block_ms)
|
|
52
|
+
next if entries.empty?
|
|
53
|
+
|
|
54
|
+
entries.each do |e|
|
|
55
|
+
begin
|
|
56
|
+
result = yield e
|
|
57
|
+
ack(e.id) if auto_ack
|
|
58
|
+
return if result == :halt
|
|
59
|
+
rescue StandardError => err
|
|
60
|
+
warn "[#{self.class.name}] error processing #{stream_key} #{e.id}: #{err.message}"
|
|
61
|
+
# Do not ack on error; pending entries can be retried/claimed later
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Acknowledge an entry id back to the consumer group.
|
|
68
|
+
def ack(entry_id)
|
|
69
|
+
@redis.xack(stream_key, group, entry_id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Inspect pending summary (XPENDING without range) for monitoring.
|
|
73
|
+
def pending_summary
|
|
74
|
+
@redis.xpending(stream_key, group)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get the consumer group lag (number of messages not yet delivered to the group).
|
|
78
|
+
# Returns Integer (lag) or nil if lag cannot be determined.
|
|
79
|
+
def lag
|
|
80
|
+
groups = @redis.xinfo(:groups, stream_key)
|
|
81
|
+
group_info = groups.find { |g| g['name'] == group }
|
|
82
|
+
return nil unless group_info
|
|
83
|
+
|
|
84
|
+
# Redis returns 'lag' field; if nil, it means calculation is pending or not applicable
|
|
85
|
+
group_info['lag']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Reset the consumer group offset to a specific ID (default '0' for beginning).
|
|
89
|
+
# This forces the consumer group to re-process messages starting after the given ID.
|
|
90
|
+
# @param id [String] new last delivered ID (e.g., '0', '$', or specific timestamp)
|
|
91
|
+
def reset_offset(id: '0')
|
|
92
|
+
@redis.xgroup(:setid, stream_key, group, id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Claim idle pending entries older than min_idle_ms for this consumer.
|
|
96
|
+
# Returns claimed Entry objects.
|
|
97
|
+
def claim_idle(min_idle_ms: 60_000, count: 10)
|
|
98
|
+
# Find pending entries from any consumer that are idle
|
|
99
|
+
summary = @redis.xpending(stream_key, group, '-', '+', count)
|
|
100
|
+
ids = Array(summary).map { |row| row.is_a?(Array) ? row[0] : row['entry_id'] }.compact
|
|
101
|
+
return [] if ids.empty?
|
|
102
|
+
|
|
103
|
+
# Claim them - this moves them to our PEL and returns the message content
|
|
104
|
+
raw = @redis.xclaim(stream_key, group, consumer, min_idle_ms, ids)
|
|
105
|
+
|
|
106
|
+
# xclaim returns entries in a slightly different format than xreadgroup
|
|
107
|
+
# Format: [[id, [k, v, ...]], ...]
|
|
108
|
+
entries = []
|
|
109
|
+
Array(raw).each do |pair|
|
|
110
|
+
id, fields = pair
|
|
111
|
+
entries << Entry.new(stream_key, id, fields_to_hash(fields))
|
|
112
|
+
end
|
|
113
|
+
entries
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Manually trim the stream to a maximum length.
|
|
117
|
+
# @param maxlen [Integer] Maximum number of entries to keep
|
|
118
|
+
# @param approximate [Boolean] Whether to use approximate trimming (~), which is more efficient
|
|
119
|
+
# @return [Integer] Number of entries removed
|
|
120
|
+
def trim(maxlen:, approximate: true)
|
|
121
|
+
@redis.xtrim(stream_key, maxlen, approximate: approximate)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Normalize Redis gem return into [Entry].
|
|
127
|
+
def normalize_read_result(raw)
|
|
128
|
+
return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
129
|
+
|
|
130
|
+
# Redis >= 5 typically returns a Hash: { stream_key => [[entry_id, [k1, v1, ...]], ...] }
|
|
131
|
+
if raw.is_a?(Hash)
|
|
132
|
+
entries = []
|
|
133
|
+
list = raw[stream_key] || []
|
|
134
|
+
list.each do |pair|
|
|
135
|
+
id, fields = pair
|
|
136
|
+
entries << Entry.new(stream_key, id, fields_to_hash(fields))
|
|
137
|
+
end
|
|
138
|
+
return entries
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Fallback for array forms
|
|
142
|
+
# [[stream, [[id, [k, v, ...]], ...]]]
|
|
143
|
+
if raw.is_a?(Array)
|
|
144
|
+
entries = []
|
|
145
|
+
raw.each do |stream_and_entries|
|
|
146
|
+
stream, pairs = stream_and_entries
|
|
147
|
+
Array(pairs).each do |pair|
|
|
148
|
+
id, fields = pair
|
|
149
|
+
entries << Entry.new(stream.to_s, id, fields_to_hash(fields))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
return entries
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
[]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def fields_to_hash(fields)
|
|
159
|
+
return fields if fields.is_a?(Hash)
|
|
160
|
+
Hash[*Array(fields)]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'stream_consumer'
|
|
4
|
+
|
|
5
|
+
module RedisExchange
|
|
6
|
+
# TradeStreamConsumer
|
|
7
|
+
# -------------------
|
|
8
|
+
# Encapsulates Redis Streams consumer-group operations for `trades:{symbol}`.
|
|
9
|
+
class TradeStreamConsumer < StreamConsumer
|
|
10
|
+
attr_reader :symbol
|
|
11
|
+
|
|
12
|
+
def initialize(redis:, symbol:, group:, consumer:)
|
|
13
|
+
@symbol = symbol
|
|
14
|
+
super(redis: redis, group: group, consumer: consumer, stream_key: "trades:#{symbol}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|