librats 2.2.0 → 2.3.1
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.
- package/lib/index.d.ts +30 -2
- package/lib/index.js +16 -1
- package/native-src/CMakeLists.txt +45 -3
- package/native-src/src/librats/bindings/rats.cpp +56 -3
- package/native-src/src/librats/bindings/rats.h +62 -1
- package/native-src/src/librats/core/types.cpp +1 -1
- package/native-src/src/librats/core/types.h +9 -6
- package/native-src/src/librats/node/config.h +9 -2
- package/native-src/src/librats/node/node.cpp +20 -5
- package/native-src/src/librats/node/node.h +26 -6
- package/native-src/src/librats/node/peer_network.h +19 -2
- package/native-src/src/librats/peer/peer.h +17 -6
- package/native-src/src/librats/security/noise_security.cpp +2 -0
- package/native-src/src/librats/security/plaintext_security.h +1 -0
- package/native-src/src/librats/security/session.h +7 -0
- package/native-src/src/librats/storage/storage.cpp +434 -213
- package/native-src/src/librats/storage/storage.h +163 -19
- package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
- package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
- package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
- package/native-src/src/librats/subsystems/relay.cpp +2 -1
- package/native-src/src/librats/transport/connection.cpp +40 -8
- package/native-src/src/librats/transport/connection.h +12 -2
- package/native-src/src/librats/util/logger.h +37 -5
- package/native-src/src/librats/util/network_monitor.cpp +14 -0
- package/native-src/src/librats/util/network_utils.cpp +10 -1
- package/package.json +1 -1
- package/src/librats_node.cpp +50 -1
|
@@ -22,6 +22,15 @@ void put_u32(std::vector<uint8_t>& b, uint32_t v) {
|
|
|
22
22
|
for (int i = 3; i >= 0; --i) b.push_back(static_cast<uint8_t>((v >> (i * 8)) & 0xFF));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
void write_u32(uint8_t* p, uint32_t v) {
|
|
26
|
+
for (int i = 0; i < 4; ++i) p[i] = static_cast<uint8_t>((v >> ((3 - i) * 8)) & 0xFF);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
uint32_t read_u32(const uint8_t* p) {
|
|
30
|
+
return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
|
|
31
|
+
(static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
} // namespace
|
|
26
35
|
|
|
27
36
|
//=============================================================================
|
|
@@ -61,9 +70,11 @@ bool StorageEntry::verify_checksum() const {
|
|
|
61
70
|
return temp.checksum == checksum;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
size_t StorageEntry::serialized_size() const {
|
|
74
|
+
return 4 + 4 + key.size() + 1 + 1 + 8 + 4 + origin_peer_id.size() + 4 + data.size() + 4;
|
|
75
|
+
}
|
|
66
76
|
|
|
77
|
+
void StorageEntry::serialize_into(std::vector<uint8_t>& buffer) const {
|
|
67
78
|
// Format:
|
|
68
79
|
// [4 bytes] total_length (excluding this field)
|
|
69
80
|
// [4 bytes] key_length
|
|
@@ -77,158 +88,111 @@ std::vector<uint8_t> StorageEntry::serialize() const {
|
|
|
77
88
|
// [data_length bytes] data
|
|
78
89
|
// [4 bytes] checksum
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
uint32_t
|
|
82
|
-
uint32_t
|
|
83
|
-
uint32_t data_len
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
buffer
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
buffer
|
|
92
|
-
buffer
|
|
93
|
-
|
|
94
|
-
// Key length (big endian)
|
|
95
|
-
buffer.push_back((key_len >> 24) & 0xFF);
|
|
96
|
-
buffer.push_back((key_len >> 16) & 0xFF);
|
|
97
|
-
buffer.push_back((key_len >> 8) & 0xFF);
|
|
98
|
-
buffer.push_back(key_len & 0xFF);
|
|
99
|
-
|
|
100
|
-
// Key
|
|
91
|
+
const uint32_t key_len = static_cast<uint32_t>(key.size());
|
|
92
|
+
const uint32_t peer_id_len = static_cast<uint32_t>(origin_peer_id.size());
|
|
93
|
+
const uint32_t data_len = static_cast<uint32_t>(data.size());
|
|
94
|
+
const uint32_t total_len = 4 + key_len + 1 + 1 + 8 + 4 + peer_id_len + 4 + data_len + 4;
|
|
95
|
+
|
|
96
|
+
// Deliberately no reserve() here. Appending entry after entry into one
|
|
97
|
+
// buffer is the whole point of this overload, and a reserve() of exactly
|
|
98
|
+
// what the next entry needs re-allocates on every single call — turning the
|
|
99
|
+
// batch into O(n^2) copying. The vector's own geometric growth is what makes
|
|
100
|
+
// the append amortised; callers that know the total up front (serialize(),
|
|
101
|
+
// the snapshot chunk) reserve once, outside the loop.
|
|
102
|
+
put_u32(buffer, total_len);
|
|
103
|
+
put_u32(buffer, key_len);
|
|
101
104
|
buffer.insert(buffer.end(), key.begin(), key.end());
|
|
102
|
-
|
|
103
|
-
// Type
|
|
104
105
|
buffer.push_back(static_cast<uint8_t>(type));
|
|
105
|
-
|
|
106
|
-
// Deleted flag
|
|
107
106
|
buffer.push_back(deleted ? 1 : 0);
|
|
108
|
-
|
|
109
|
-
// Timestamp (big endian)
|
|
110
107
|
for (int i = 7; i >= 0; i--) {
|
|
111
108
|
buffer.push_back(static_cast<uint8_t>((timestamp_ms >> (i * 8)) & 0xFF));
|
|
112
109
|
}
|
|
113
|
-
|
|
114
|
-
// Peer ID length (big endian)
|
|
115
|
-
buffer.push_back((peer_id_len >> 24) & 0xFF);
|
|
116
|
-
buffer.push_back((peer_id_len >> 16) & 0xFF);
|
|
117
|
-
buffer.push_back((peer_id_len >> 8) & 0xFF);
|
|
118
|
-
buffer.push_back(peer_id_len & 0xFF);
|
|
119
|
-
|
|
120
|
-
// Peer ID
|
|
110
|
+
put_u32(buffer, peer_id_len);
|
|
121
111
|
buffer.insert(buffer.end(), origin_peer_id.begin(), origin_peer_id.end());
|
|
122
|
-
|
|
123
|
-
// Data length (big endian)
|
|
124
|
-
buffer.push_back((data_len >> 24) & 0xFF);
|
|
125
|
-
buffer.push_back((data_len >> 16) & 0xFF);
|
|
126
|
-
buffer.push_back((data_len >> 8) & 0xFF);
|
|
127
|
-
buffer.push_back(data_len & 0xFF);
|
|
128
|
-
|
|
129
|
-
// Data
|
|
112
|
+
put_u32(buffer, data_len);
|
|
130
113
|
buffer.insert(buffer.end(), data.begin(), data.end());
|
|
114
|
+
put_u32(buffer, checksum);
|
|
115
|
+
}
|
|
131
116
|
|
|
132
|
-
|
|
133
|
-
buffer
|
|
134
|
-
buffer.
|
|
135
|
-
buffer
|
|
136
|
-
buffer.push_back(checksum & 0xFF);
|
|
137
|
-
|
|
117
|
+
std::vector<uint8_t> StorageEntry::serialize() const {
|
|
118
|
+
std::vector<uint8_t> buffer;
|
|
119
|
+
buffer.reserve(serialized_size());
|
|
120
|
+
serialize_into(buffer);
|
|
138
121
|
return buffer;
|
|
139
122
|
}
|
|
140
123
|
|
|
141
|
-
bool StorageEntry::deserialize(const
|
|
124
|
+
bool StorageEntry::deserialize(const uint8_t* buffer, size_t size, size_t offset,
|
|
142
125
|
StorageEntry& entry, size_t& bytes_read) {
|
|
143
126
|
bytes_read = 0;
|
|
144
127
|
|
|
145
|
-
|
|
146
|
-
if (offset + 4 > buffer.size()) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Read total length
|
|
151
|
-
uint32_t total_len = (static_cast<uint32_t>(buffer[offset]) << 24) |
|
|
152
|
-
(static_cast<uint32_t>(buffer[offset + 1]) << 16) |
|
|
153
|
-
(static_cast<uint32_t>(buffer[offset + 2]) << 8) |
|
|
154
|
-
static_cast<uint32_t>(buffer[offset + 3]);
|
|
128
|
+
if (offset > size || size - offset < 4) return false;
|
|
155
129
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
130
|
+
// The entry declares its own length; every field below is read against that
|
|
131
|
+
// end rather than against the end of the buffer, so an entry can neither
|
|
132
|
+
// overrun the buffer nor reach into the entry that follows it in a batch.
|
|
133
|
+
const uint32_t total_len = read_u32(buffer + offset);
|
|
134
|
+
if (size - offset - 4 < total_len) return false;
|
|
160
135
|
|
|
161
|
-
size_t
|
|
136
|
+
size_t pos = offset + 4;
|
|
137
|
+
const size_t end = pos + total_len;
|
|
162
138
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
139
|
+
// Reads the next `n` bytes if the entry still declares that many.
|
|
140
|
+
const auto take = [&](size_t n) -> const uint8_t* {
|
|
141
|
+
if (end - pos < n) return nullptr;
|
|
142
|
+
const uint8_t* p = buffer + pos;
|
|
143
|
+
pos += n;
|
|
144
|
+
return p;
|
|
145
|
+
};
|
|
170
146
|
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
pos += key_len;
|
|
147
|
+
const uint8_t* p = take(4);
|
|
148
|
+
if (!p) return false;
|
|
149
|
+
const uint32_t key_len = read_u32(p);
|
|
175
150
|
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
entry.
|
|
179
|
-
pos += 1;
|
|
151
|
+
p = take(key_len);
|
|
152
|
+
if (!p) return false;
|
|
153
|
+
entry.key.assign(reinterpret_cast<const char*>(p), key_len);
|
|
180
154
|
|
|
181
|
-
//
|
|
182
|
-
if (
|
|
183
|
-
entry.
|
|
184
|
-
|
|
155
|
+
p = take(2); // type + deleted flag
|
|
156
|
+
if (!p) return false;
|
|
157
|
+
entry.type = static_cast<StorageValueType>(p[0]);
|
|
158
|
+
entry.deleted = p[1] != 0;
|
|
185
159
|
|
|
186
|
-
|
|
187
|
-
if (
|
|
160
|
+
p = take(8);
|
|
161
|
+
if (!p) return false;
|
|
188
162
|
entry.timestamp_ms = 0;
|
|
189
|
-
for (int i = 0; i < 8; i++)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Read data
|
|
216
|
-
if (pos + data_len > buffer.size()) return false;
|
|
217
|
-
entry.data = std::vector<uint8_t>(buffer.begin() + pos, buffer.begin() + pos + data_len);
|
|
218
|
-
pos += data_len;
|
|
219
|
-
|
|
220
|
-
// Read checksum
|
|
221
|
-
if (pos + 4 > buffer.size()) return false;
|
|
222
|
-
entry.checksum = (static_cast<uint32_t>(buffer[pos]) << 24) |
|
|
223
|
-
(static_cast<uint32_t>(buffer[pos + 1]) << 16) |
|
|
224
|
-
(static_cast<uint32_t>(buffer[pos + 2]) << 8) |
|
|
225
|
-
static_cast<uint32_t>(buffer[pos + 3]);
|
|
226
|
-
pos += 4;
|
|
227
|
-
|
|
228
|
-
bytes_read = pos - offset;
|
|
163
|
+
for (int i = 0; i < 8; i++) entry.timestamp_ms = (entry.timestamp_ms << 8) | p[i];
|
|
164
|
+
|
|
165
|
+
p = take(4);
|
|
166
|
+
if (!p) return false;
|
|
167
|
+
const uint32_t peer_id_len = read_u32(p);
|
|
168
|
+
|
|
169
|
+
p = take(peer_id_len);
|
|
170
|
+
if (!p) return false;
|
|
171
|
+
entry.origin_peer_id.assign(reinterpret_cast<const char*>(p), peer_id_len);
|
|
172
|
+
|
|
173
|
+
p = take(4);
|
|
174
|
+
if (!p) return false;
|
|
175
|
+
const uint32_t data_len = read_u32(p);
|
|
176
|
+
|
|
177
|
+
p = take(data_len);
|
|
178
|
+
if (!p) return false;
|
|
179
|
+
entry.data.assign(p, p + data_len);
|
|
180
|
+
|
|
181
|
+
p = take(4);
|
|
182
|
+
if (!p) return false;
|
|
183
|
+
entry.checksum = read_u32(p);
|
|
184
|
+
|
|
185
|
+
// Trailing bytes inside the declared length are skipped, not rejected: that
|
|
186
|
+
// is what lets a field be appended to the format without a version bump.
|
|
187
|
+
bytes_read = end - offset;
|
|
229
188
|
return true;
|
|
230
189
|
}
|
|
231
190
|
|
|
191
|
+
bool StorageEntry::deserialize(const std::vector<uint8_t>& buffer, size_t offset,
|
|
192
|
+
StorageEntry& entry, size_t& bytes_read) {
|
|
193
|
+
return deserialize(buffer.data(), buffer.size(), offset, entry, bytes_read);
|
|
194
|
+
}
|
|
195
|
+
|
|
232
196
|
bool StorageEntry::wins_over(const StorageEntry& other) const {
|
|
233
197
|
// Last-Write-Wins: compare timestamps first
|
|
234
198
|
if (timestamp_ms != other.timestamp_ms) {
|
|
@@ -267,6 +231,19 @@ StorageValueType string_to_storage_value_type(const std::string& str) {
|
|
|
267
231
|
// StorageManager Implementation
|
|
268
232
|
//=============================================================================
|
|
269
233
|
|
|
234
|
+
void StorageManager::sanitize_config(StorageConfig& config) {
|
|
235
|
+
// A value or a chunk bigger than what one send queue reports room for would
|
|
236
|
+
// make the very first one it travels on unwritable — and two of them would
|
|
237
|
+
// trip the high-water mark and drop the peer. Clamp rather than reject: the
|
|
238
|
+
// limits are a safety belt, not something a caller tunes for correctness.
|
|
239
|
+
config.max_value_size =
|
|
240
|
+
(std::min)(config.max_value_size, StorageConfig::kMaxValueSize);
|
|
241
|
+
config.sync_batch_bytes =
|
|
242
|
+
(std::min)(config.sync_batch_bytes, StorageConfig::kMaxSyncBatchBytes);
|
|
243
|
+
config.sync_batch_bytes =
|
|
244
|
+
(std::max)(config.sync_batch_bytes, StorageConfig::kMinSyncBatchBytes);
|
|
245
|
+
}
|
|
246
|
+
|
|
270
247
|
StorageManager::StorageManager(const StorageConfig& config)
|
|
271
248
|
: config_(config),
|
|
272
249
|
sync_status_(StorageSyncStatus::NOT_STARTED),
|
|
@@ -274,6 +251,8 @@ StorageManager::StorageManager(const StorageConfig& config)
|
|
|
274
251
|
running_(true),
|
|
275
252
|
dirty_(false) {
|
|
276
253
|
|
|
254
|
+
sanitize_config(config_);
|
|
255
|
+
|
|
277
256
|
// Initialize statistics
|
|
278
257
|
stats_ = StorageStatistics();
|
|
279
258
|
stats_.sync_status = StorageSyncStatus::NOT_STARTED;
|
|
@@ -302,6 +281,8 @@ void StorageManager::shutdown() {
|
|
|
302
281
|
|
|
303
282
|
LOG_STORAGE_INFO("StorageManager shutting down...");
|
|
304
283
|
|
|
284
|
+
stop_sync_thread();
|
|
285
|
+
|
|
305
286
|
// Wake up persistence thread
|
|
306
287
|
{
|
|
307
288
|
std::lock_guard<std::mutex> lock(persistence_mutex_);
|
|
@@ -357,20 +338,47 @@ void StorageManager::attach(NodeContext& ctx) {
|
|
|
357
338
|
[this](const Peer& peer, ByteView payload) { on_storage_message(peer.id(), payload); });
|
|
358
339
|
network_->on_peer_connected(
|
|
359
340
|
[this](const Peer& peer) { on_peer_connected(peer.id()); });
|
|
341
|
+
// Without this the per-peer sync state of every peer that ever connected
|
|
342
|
+
// would be kept forever, snapshot cursors and all.
|
|
343
|
+
network_->on_peer_disconnected(
|
|
344
|
+
[this](const PeerId& id, CloseReason) { on_peer_disconnected(id); });
|
|
345
|
+
// The other half of honouring send()'s return value: a peer that filled up is
|
|
346
|
+
// owed a snapshot, and this is what says the link has room to serve it.
|
|
347
|
+
network_->on_peer_writable(
|
|
348
|
+
[this](const Peer& peer) { on_peer_writable(peer.id()); });
|
|
360
349
|
}
|
|
361
350
|
|
|
362
351
|
void StorageManager::start() {
|
|
363
|
-
// The persistence thread is already running (started in the constructor)
|
|
364
|
-
// sync
|
|
352
|
+
// The persistence thread is already running (started in the constructor).
|
|
353
|
+
// The sync thread only exists once there is a network to pace against.
|
|
354
|
+
if (config_.enable_sync && network_) start_sync_thread();
|
|
365
355
|
}
|
|
366
356
|
|
|
367
357
|
void StorageManager::stop() {
|
|
368
358
|
shutdown();
|
|
369
359
|
}
|
|
370
360
|
|
|
361
|
+
void StorageManager::start_sync_thread() {
|
|
362
|
+
if (sync_running_.exchange(true)) return;
|
|
363
|
+
batch_bytes_ = config_.sync_batch_bytes;
|
|
364
|
+
sync_interval_ = std::chrono::milliseconds(config_.sync_min_interval_ms);
|
|
365
|
+
sync_thread_ = std::thread(&StorageManager::sync_thread_loop, this);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
void StorageManager::stop_sync_thread() {
|
|
369
|
+
if (!sync_running_.exchange(false)) return;
|
|
370
|
+
// Taken and dropped for the ordering alone: it puts the flag store above
|
|
371
|
+
// before any wait the thread is about to enter, so the notify below cannot
|
|
372
|
+
// slip past a thread that had already decided to sleep.
|
|
373
|
+
{ std::lock_guard<std::mutex> lock(sync_mutex_); }
|
|
374
|
+
sync_cv_.notify_all();
|
|
375
|
+
if (sync_thread_.joinable()) sync_thread_.join();
|
|
376
|
+
}
|
|
377
|
+
|
|
371
378
|
void StorageManager::set_config(const StorageConfig& config) {
|
|
372
379
|
std::lock_guard<std::mutex> lock(storage_mutex_);
|
|
373
380
|
config_ = config;
|
|
381
|
+
sanitize_config(config_);
|
|
374
382
|
|
|
375
383
|
if (config_.persist_to_disk) {
|
|
376
384
|
create_directories(config_.data_directory.c_str());
|
|
@@ -646,14 +654,12 @@ std::vector<std::string> StorageManager::keys() const {
|
|
|
646
654
|
std::vector<std::string> StorageManager::keys_with_prefix(const std::string& prefix) const {
|
|
647
655
|
std::lock_guard<std::mutex> lock(storage_mutex_);
|
|
648
656
|
|
|
657
|
+
// A range scan, not a full walk: the map is ordered, so the matching keys are
|
|
658
|
+
// exactly the contiguous run starting at lower_bound(prefix).
|
|
649
659
|
std::vector<std::string> result;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (!
|
|
653
|
-
pair.first.size() >= prefix.size() &&
|
|
654
|
-
pair.first.compare(0, prefix.size(), prefix) == 0) {
|
|
655
|
-
result.push_back(pair.first);
|
|
656
|
-
}
|
|
660
|
+
for (auto it = entries_.lower_bound(prefix); it != entries_.end(); ++it) {
|
|
661
|
+
if (it->first.compare(0, prefix.size(), prefix) != 0) break;
|
|
662
|
+
if (!it->second.deleted) result.push_back(it->first);
|
|
657
663
|
}
|
|
658
664
|
|
|
659
665
|
return result;
|
|
@@ -865,6 +871,9 @@ librats::Json StorageManager::get_statistics_json() const {
|
|
|
865
871
|
result["entries_sent"] = stats.entries_sent;
|
|
866
872
|
result["sync_requests_received"] = stats.sync_requests_received;
|
|
867
873
|
result["sync_requests_sent"] = stats.sync_requests_sent;
|
|
874
|
+
result["sync_chunks_sent"] = stats.sync_chunks_sent;
|
|
875
|
+
result["sync_chunks_received"] = stats.sync_chunks_received;
|
|
876
|
+
result["resyncs_scheduled"] = stats.resyncs_scheduled;
|
|
868
877
|
|
|
869
878
|
switch (stats.sync_status) {
|
|
870
879
|
case StorageSyncStatus::NOT_STARTED: result["sync_status"] = "not_started"; break;
|
|
@@ -882,17 +891,21 @@ librats::Json StorageManager::get_statistics_json() const {
|
|
|
882
891
|
|
|
883
892
|
void StorageManager::on_storage_message(const PeerId& from, ByteView payload) {
|
|
884
893
|
if (payload.empty()) return;
|
|
894
|
+
if (payload.size() > kMaxInboundMessage) {
|
|
895
|
+
LOG_STORAGE_WARN("Oversized storage message (" << payload.size() << " B) from "
|
|
896
|
+
<< from.short_hex() << "; ignored");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
885
899
|
|
|
886
900
|
const uint8_t* p = payload.data();
|
|
887
|
-
const size_t
|
|
888
|
-
const uint8_t op = p[0];
|
|
901
|
+
const size_t n = payload.size();
|
|
889
902
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
903
|
+
switch (p[0]) {
|
|
904
|
+
case OP_ENTRY: {
|
|
905
|
+
// Parsed straight out of the receive buffer — no copy of the payload.
|
|
893
906
|
StorageEntry entry;
|
|
894
907
|
size_t bytes_read = 0;
|
|
895
|
-
if (!StorageEntry::deserialize(
|
|
908
|
+
if (!StorageEntry::deserialize(p, n, 1, entry, bytes_read)) {
|
|
896
909
|
LOG_STORAGE_WARN("Malformed storage entry from " << from.short_hex());
|
|
897
910
|
return;
|
|
898
911
|
}
|
|
@@ -912,66 +925,119 @@ void StorageManager::on_storage_message(const PeerId& from, ByteView payload) {
|
|
|
912
925
|
// Re-flood to other peers; LWW makes a duplicate lose, so this stops.
|
|
913
926
|
forward_entry(entry, from);
|
|
914
927
|
}
|
|
915
|
-
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
case OP_SYNC_REQUEST: {
|
|
916
932
|
{
|
|
917
933
|
std::lock_guard<std::mutex> lock(stats_mutex_);
|
|
918
934
|
stats_.sync_requests_received++;
|
|
919
935
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
(static_cast<uint32_t>(p[3]) << 8) |
|
|
927
|
-
static_cast<uint32_t>(p[4]);
|
|
928
|
-
std::vector<uint8_t> buf(p + 5, p + n);
|
|
929
|
-
size_t offset = 0;
|
|
930
|
-
int applied = 0;
|
|
931
|
-
for (uint32_t i = 0; i < count && offset < buf.size(); i++) {
|
|
932
|
-
StorageEntry entry;
|
|
933
|
-
size_t bytes_read = 0;
|
|
934
|
-
if (!StorageEntry::deserialize(buf, offset, entry, bytes_read)) break;
|
|
935
|
-
offset += bytes_read;
|
|
936
|
-
if (!entry.verify_checksum()) continue;
|
|
936
|
+
// Queue the work; the sync thread serializes and paces it. Serializing
|
|
937
|
+
// the database here would be doing it on a reactor thread, under
|
|
938
|
+
// storage_mutex_, for as long as the database is big.
|
|
939
|
+
schedule_snapshot(from, /*requested_by_peer=*/true);
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
937
942
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
+
case OP_SYNC_CHUNK: {
|
|
944
|
+
// [3][flags:u8][count:u32][entry]*
|
|
945
|
+
if (n < 6) return;
|
|
946
|
+
const bool last = (p[1] & FLAG_LAST) != 0;
|
|
947
|
+
const uint32_t count = read_u32(p + 2);
|
|
948
|
+
|
|
949
|
+
const uint32_t applied = apply_chunk(from, p + 6, n - 6, count);
|
|
950
|
+
if (applied > 0) mark_dirty();
|
|
951
|
+
{
|
|
952
|
+
std::lock_guard<std::mutex> lock(stats_mutex_);
|
|
953
|
+
stats_.entries_synced += applied;
|
|
954
|
+
stats_.sync_chunks_received++;
|
|
943
955
|
}
|
|
944
956
|
|
|
957
|
+
LOG_STORAGE_DEBUG("Snapshot chunk from " << from.short_hex() << ": " << count
|
|
958
|
+
<< " entries, " << applied << " applied" << (last ? " (last)" : ""));
|
|
959
|
+
if (!last) break;
|
|
960
|
+
|
|
945
961
|
{
|
|
946
962
|
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
947
|
-
sync_status_
|
|
963
|
+
sync_status_ = StorageSyncStatus::COMPLETED;
|
|
948
964
|
initial_sync_complete_ = true;
|
|
949
|
-
last_sync_time_
|
|
965
|
+
last_sync_time_ = std::chrono::steady_clock::now();
|
|
950
966
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
967
|
+
LOG_STORAGE_INFO("Snapshot from " << from.short_hex() << " complete");
|
|
968
|
+
if (sync_complete_callback_) sync_complete_callback_(true, "");
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
default:
|
|
973
|
+
LOG_STORAGE_WARN("Unknown storage opcode " << static_cast<int>(p[0])
|
|
974
|
+
<< " from " << from.short_hex());
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
uint32_t StorageManager::apply_chunk(const PeerId& from, const uint8_t* data, size_t size,
|
|
980
|
+
uint32_t count) {
|
|
981
|
+
uint32_t applied = 0;
|
|
982
|
+
size_t offset = 0;
|
|
983
|
+
|
|
984
|
+
for (uint32_t i = 0; i < count && offset < size; i++) {
|
|
985
|
+
StorageEntry entry;
|
|
986
|
+
size_t bytes_read = 0;
|
|
987
|
+
if (!StorageEntry::deserialize(data, size, offset, entry, bytes_read)) {
|
|
988
|
+
LOG_STORAGE_WARN("Truncated snapshot chunk from " << from.short_hex()
|
|
989
|
+
<< " at entry " << i);
|
|
990
|
+
break;
|
|
954
991
|
}
|
|
955
|
-
|
|
992
|
+
offset += bytes_read;
|
|
993
|
+
if (!entry.verify_checksum()) continue;
|
|
956
994
|
|
|
957
|
-
|
|
958
|
-
if (
|
|
995
|
+
StorageChangeEvent event;
|
|
996
|
+
if (apply_remote_entry(entry, &event)) {
|
|
997
|
+
applied++;
|
|
998
|
+
notify_change(event);
|
|
999
|
+
}
|
|
959
1000
|
}
|
|
1001
|
+
return applied;
|
|
960
1002
|
}
|
|
961
1003
|
|
|
962
1004
|
void StorageManager::on_peer_connected(const PeerId& peer_id) {
|
|
963
1005
|
if (!config_.enable_sync) return;
|
|
964
1006
|
|
|
965
1007
|
// Anti-entropy: ask the new peer for a full snapshot. Both ends do this on
|
|
966
|
-
// connect, so the two databases converge via LWW.
|
|
1008
|
+
// connect, so the two databases converge via LWW. Both snapshots are streams
|
|
1009
|
+
// paced against their own link, so the pair crossing costs bandwidth and
|
|
1010
|
+
// nothing else.
|
|
967
1011
|
{
|
|
968
1012
|
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
969
1013
|
if (sync_status_ == StorageSyncStatus::NOT_STARTED)
|
|
970
1014
|
sync_status_ = StorageSyncStatus::IN_PROGRESS;
|
|
1015
|
+
peers_.emplace(peer_id, PeerSync{});
|
|
971
1016
|
}
|
|
972
1017
|
send_sync_request(peer_id);
|
|
973
1018
|
}
|
|
974
1019
|
|
|
1020
|
+
void StorageManager::on_peer_disconnected(const PeerId& peer_id) {
|
|
1021
|
+
{
|
|
1022
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1023
|
+
if (peers_.erase(peer_id) == 0) return; // drops any snapshot in flight to it
|
|
1024
|
+
++sync_epoch_;
|
|
1025
|
+
}
|
|
1026
|
+
sync_cv_.notify_all();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
void StorageManager::on_peer_writable(const PeerId& peer_id) {
|
|
1030
|
+
// The link drained. Either a snapshot was waiting for room, or one is owed
|
|
1031
|
+
// because live entries were dropped while it was full — the sync thread
|
|
1032
|
+
// decides which; all this has to do is wake it.
|
|
1033
|
+
{
|
|
1034
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1035
|
+
if (peers_.find(peer_id) == peers_.end()) return;
|
|
1036
|
+
++sync_epoch_;
|
|
1037
|
+
}
|
|
1038
|
+
sync_cv_.notify_all();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
975
1041
|
//=============================================================================
|
|
976
1042
|
// Private Methods - Serialization
|
|
977
1043
|
//=============================================================================
|
|
@@ -1029,42 +1095,59 @@ std::string StorageManager::deserialize_string(const std::vector<uint8_t>& data)
|
|
|
1029
1095
|
// Private Methods - Network Operations
|
|
1030
1096
|
//=============================================================================
|
|
1031
1097
|
|
|
1032
|
-
void StorageManager::
|
|
1033
|
-
if (!network_) return;
|
|
1098
|
+
void StorageManager::replicate_entry(const StorageEntry& entry, const PeerId* except) {
|
|
1099
|
+
if (!network_ || !config_.enable_sync) return;
|
|
1034
1100
|
|
|
1035
1101
|
std::vector<uint8_t> msg;
|
|
1102
|
+
msg.reserve(1 + entry.serialized_size());
|
|
1036
1103
|
msg.push_back(OP_ENTRY);
|
|
1037
|
-
|
|
1038
|
-
|
|
1104
|
+
entry.serialize_into(msg);
|
|
1105
|
+
const ByteView view(msg);
|
|
1039
1106
|
|
|
1040
|
-
|
|
1107
|
+
uint64_t sent = 0;
|
|
1108
|
+
std::vector<PeerId> congested;
|
|
1041
1109
|
|
|
1042
|
-
{
|
|
1043
|
-
|
|
1044
|
-
stats_.entries_sent++;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1110
|
+
for (const PeerId& peer : network_->connected_peers()) {
|
|
1111
|
+
if (except && peer == *except) continue;
|
|
1047
1112
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1113
|
+
// A peer already owed a snapshot is skipped outright: it is behind by
|
|
1114
|
+
// more than this entry, and the snapshot that is coming carries the
|
|
1115
|
+
// winning state for this key too.
|
|
1116
|
+
{
|
|
1117
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1118
|
+
const auto it = peers_.find(peer);
|
|
1119
|
+
if (it != peers_.end() && it->second.owed) continue;
|
|
1120
|
+
}
|
|
1050
1121
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1122
|
+
// The message is queued either way; a false says the queue is past the
|
|
1123
|
+
// mark and that continuing is what gets the peer dropped. So we stop
|
|
1124
|
+
// sending this peer individual entries and owe it a snapshot instead —
|
|
1125
|
+
// safe because the store is LWW and the snapshot is the whole state.
|
|
1126
|
+
if (network_->send(peer, MessageType::Storage, view)) {
|
|
1127
|
+
++sent;
|
|
1128
|
+
} else {
|
|
1129
|
+
congested.push_back(peer);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1056
1132
|
|
|
1057
|
-
for (const PeerId& peer :
|
|
1058
|
-
|
|
1059
|
-
|
|
1133
|
+
for (const PeerId& peer : congested) schedule_snapshot(peer, /*requested_by_peer=*/false);
|
|
1134
|
+
|
|
1135
|
+
if (sent > 0) {
|
|
1136
|
+
std::lock_guard<std::mutex> lock(stats_mutex_);
|
|
1137
|
+
stats_.entries_sent += sent;
|
|
1060
1138
|
}
|
|
1061
1139
|
}
|
|
1062
1140
|
|
|
1063
1141
|
void StorageManager::send_sync_request(const PeerId& peer_id) {
|
|
1064
1142
|
if (!network_) return;
|
|
1065
1143
|
|
|
1066
|
-
std::vector<uint8_t> msg{OP_SYNC_REQUEST};
|
|
1067
|
-
|
|
1144
|
+
const std::vector<uint8_t> msg{OP_SYNC_REQUEST};
|
|
1145
|
+
// One byte cannot fill a queue on its own, so a false here means the queue
|
|
1146
|
+
// was already full — the peer will ask us for a snapshot on its own side
|
|
1147
|
+
// anyway, and this request is retried the next time it connects.
|
|
1148
|
+
if (!network_->send(peer_id, MessageType::Storage, ByteView(msg))) {
|
|
1149
|
+
LOG_STORAGE_DEBUG("Sync request to " << peer_id.short_hex() << " queued behind a full link");
|
|
1150
|
+
}
|
|
1068
1151
|
|
|
1069
1152
|
{
|
|
1070
1153
|
std::lock_guard<std::mutex> lock(stats_mutex_);
|
|
@@ -1074,32 +1157,168 @@ void StorageManager::send_sync_request(const PeerId& peer_id) {
|
|
|
1074
1157
|
LOG_STORAGE_DEBUG("Sent sync request to peer " << peer_id.short_hex());
|
|
1075
1158
|
}
|
|
1076
1159
|
|
|
1077
|
-
void StorageManager::
|
|
1078
|
-
|
|
1160
|
+
void StorageManager::schedule_snapshot(const PeerId& peer, bool requested_by_peer) {
|
|
1161
|
+
{
|
|
1162
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1163
|
+
PeerSync& st = peers_[peer];
|
|
1164
|
+
if (st.streaming || st.owed) return; // one snapshot at a time, per peer
|
|
1165
|
+
st.owed = true;
|
|
1166
|
+
++sync_epoch_;
|
|
1167
|
+
}
|
|
1168
|
+
if (!requested_by_peer) {
|
|
1169
|
+
std::lock_guard<std::mutex> lock(stats_mutex_);
|
|
1170
|
+
stats_.resyncs_scheduled++;
|
|
1171
|
+
}
|
|
1172
|
+
sync_cv_.notify_all();
|
|
1173
|
+
}
|
|
1079
1174
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1175
|
+
//=============================================================================
|
|
1176
|
+
// The sync thread — every snapshot chunk is serialized and paced here, never
|
|
1177
|
+
// on a reactor thread.
|
|
1178
|
+
//=============================================================================
|
|
1179
|
+
|
|
1180
|
+
void StorageManager::sync_thread_loop() {
|
|
1181
|
+
// How long to leave a link that had no room before asking it again. There is
|
|
1182
|
+
// no event for bytes a caller handed over that the reactor never had to
|
|
1183
|
+
// queue, so writability is polled rather than waited on — see
|
|
1184
|
+
// PeerNetwork::peer_writable.
|
|
1185
|
+
constexpr auto kBlockedPoll = std::chrono::milliseconds(20);
|
|
1186
|
+
const auto never = (std::chrono::steady_clock::time_point::max)();
|
|
1187
|
+
|
|
1188
|
+
while (sync_running_.load()) {
|
|
1189
|
+
const auto now = std::chrono::steady_clock::now();
|
|
1190
|
+
|
|
1191
|
+
// Promote what has come due, and collect what can be streamed now. The
|
|
1192
|
+
// network is never called under sync_mutex_, so writability is tested
|
|
1193
|
+
// once the lock is dropped.
|
|
1194
|
+
std::vector<PeerId> streaming;
|
|
1195
|
+
auto wake_at = never;
|
|
1196
|
+
uint64_t epoch = 0;
|
|
1197
|
+
{
|
|
1198
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1199
|
+
epoch = sync_epoch_;
|
|
1200
|
+
for (auto& [id, st] : peers_) {
|
|
1201
|
+
if (!st.streaming && st.owed) {
|
|
1202
|
+
const auto ready_at = st.last_start + sync_interval_;
|
|
1203
|
+
if (st.last_start.time_since_epoch().count() != 0 && now < ready_at) {
|
|
1204
|
+
wake_at = (std::min)(wake_at, ready_at); // still cooling down
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
st.streaming = true;
|
|
1208
|
+
st.started = false;
|
|
1209
|
+
st.cursor.clear();
|
|
1210
|
+
st.owed = false;
|
|
1211
|
+
st.last_start = now;
|
|
1212
|
+
}
|
|
1213
|
+
if (st.streaming) streaming.push_back(id);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
bool progressed = false;
|
|
1218
|
+
for (const PeerId& peer : streaming) {
|
|
1219
|
+
if (!sync_running_.load()) return;
|
|
1220
|
+
if (!network_->peer_writable(peer)) {
|
|
1221
|
+
wake_at = (std::min)(wake_at, now + kBlockedPoll);
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
switch (stream_snapshot_chunk(peer)) {
|
|
1225
|
+
case ChunkResult::Continue: progressed = true; break;
|
|
1226
|
+
case ChunkResult::Blocked: wake_at = (std::min)(wake_at, now + kBlockedPoll); break;
|
|
1227
|
+
case ChunkResult::Finished: break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (progressed) continue; // more to send and room to send it: no wait
|
|
1231
|
+
|
|
1232
|
+
std::unique_lock<std::mutex> lock(sync_mutex_);
|
|
1233
|
+
if (!sync_running_.load()) return;
|
|
1234
|
+
// Everything above ran outside this lock, so a snapshot may have been
|
|
1235
|
+
// scheduled — and its notify already delivered to nobody — since the scan.
|
|
1236
|
+
// The epoch is what catches that; without it a wait() here could be a
|
|
1237
|
+
// wait forever with work sitting in the map.
|
|
1238
|
+
if (sync_epoch_ != epoch) continue;
|
|
1239
|
+
if (wake_at == never) sync_cv_.wait(lock);
|
|
1240
|
+
else sync_cv_.wait_until(lock, wake_at);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
StorageManager::ChunkResult StorageManager::stream_snapshot_chunk(const PeerId& peer) {
|
|
1245
|
+
std::string cursor;
|
|
1246
|
+
bool started = false;
|
|
1247
|
+
{
|
|
1248
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1249
|
+
const auto it = peers_.find(peer);
|
|
1250
|
+
if (it == peers_.end() || !it->second.streaming) return ChunkResult::Finished;
|
|
1251
|
+
cursor = it->second.cursor;
|
|
1252
|
+
started = it->second.started;
|
|
1253
|
+
}
|
|
1082
1254
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1255
|
+
std::vector<uint8_t> msg;
|
|
1256
|
+
msg.reserve(batch_bytes_ + 64);
|
|
1257
|
+
msg.push_back(OP_SYNC_CHUNK);
|
|
1258
|
+
msg.push_back(0); // flags, filled in below
|
|
1259
|
+
put_u32(msg, 0); // count, filled in below
|
|
1260
|
+
|
|
1261
|
+
uint32_t count = 0;
|
|
1262
|
+
std::string last = cursor;
|
|
1263
|
+
bool done = false;
|
|
1085
1264
|
{
|
|
1265
|
+
// The one place the whole database is walked, and it is walked a chunk at
|
|
1266
|
+
// a time: storage_mutex_ is held for `batch_bytes_` worth of work and no
|
|
1267
|
+
// more, on this thread rather than a reactor's. Between chunks nothing is
|
|
1268
|
+
// held at all — the cursor alone resumes the walk, so writes landing
|
|
1269
|
+
// mid-snapshot neither block nor derail it.
|
|
1086
1270
|
std::lock_guard<std::mutex> lock(storage_mutex_);
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1271
|
+
auto it = started ? entries_.upper_bound(cursor) : entries_.begin();
|
|
1272
|
+
while (it != entries_.end()) {
|
|
1273
|
+
it->second.serialize_into(msg);
|
|
1274
|
+
last = it->first;
|
|
1275
|
+
++count;
|
|
1276
|
+
++it;
|
|
1277
|
+
// Checked after appending, so an entry larger than the target still
|
|
1278
|
+
// goes out on its own instead of stalling the walk forever.
|
|
1279
|
+
if (msg.size() >= batch_bytes_) break;
|
|
1091
1280
|
}
|
|
1281
|
+
done = (it == entries_.end());
|
|
1092
1282
|
}
|
|
1093
1283
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1284
|
+
if (done) msg[1] = FLAG_LAST;
|
|
1285
|
+
write_u32(msg.data() + 2, count);
|
|
1096
1286
|
|
|
1097
|
-
network_->send(
|
|
1287
|
+
const bool room = network_->send(peer, MessageType::Storage, ByteView(msg));
|
|
1098
1288
|
|
|
1099
|
-
|
|
1289
|
+
{
|
|
1290
|
+
std::lock_guard<std::mutex> lock(sync_mutex_);
|
|
1291
|
+
const auto it = peers_.find(peer);
|
|
1292
|
+
// The peer may have disconnected while the chunk was being built; its
|
|
1293
|
+
// state is gone and this chunk was the last of it.
|
|
1294
|
+
if (it == peers_.end()) return ChunkResult::Finished;
|
|
1295
|
+
it->second.cursor = last;
|
|
1296
|
+
it->second.started = true;
|
|
1297
|
+
if (done) it->second.streaming = false;
|
|
1298
|
+
}
|
|
1299
|
+
{
|
|
1300
|
+
std::lock_guard<std::mutex> lock(stats_mutex_);
|
|
1301
|
+
stats_.sync_chunks_sent++;
|
|
1302
|
+
stats_.entries_sent += count;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
LOG_STORAGE_DEBUG("Snapshot chunk to " << peer.short_hex() << ": " << count << " entries, "
|
|
1306
|
+
<< msg.size() << " B" << (done ? " (last)" : ""));
|
|
1307
|
+
if (done) return ChunkResult::Finished;
|
|
1308
|
+
return room ? ChunkResult::Continue : ChunkResult::Blocked;
|
|
1100
1309
|
}
|
|
1101
1310
|
|
|
1102
1311
|
bool StorageManager::apply_remote_entry(const StorageEntry& entry, StorageChangeEvent* out_event) {
|
|
1312
|
+
// The same limit a local put() is held to. Without it a peer could push a
|
|
1313
|
+
// value this node would then have to re-serialize into every snapshot it
|
|
1314
|
+
// serves — and one big enough to blow through a send queue on its way out.
|
|
1315
|
+
if (entry.data.size() > config_.max_value_size) {
|
|
1316
|
+
LOG_STORAGE_WARN("Remote entry '" << entry.key << "' of " << entry.data.size()
|
|
1317
|
+
<< " B exceeds maximum " << config_.max_value_size << "; rejected");
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
if (entry.key.empty()) return false;
|
|
1321
|
+
|
|
1103
1322
|
StorageChangeEvent event;
|
|
1104
1323
|
event.operation = entry.deleted ? StorageOperation::OP_DELETE : StorageOperation::OP_PUT;
|
|
1105
1324
|
event.key = entry.key;
|
|
@@ -1178,9 +1397,11 @@ bool StorageManager::write_data_file() {
|
|
|
1178
1397
|
};
|
|
1179
1398
|
fwrite(count_bytes, 1, 4, file);
|
|
1180
1399
|
|
|
1181
|
-
// Write each entry
|
|
1400
|
+
// Write each entry, reusing one buffer rather than allocating per entry.
|
|
1401
|
+
std::vector<uint8_t> serialized;
|
|
1182
1402
|
for (const auto& pair : entries_) {
|
|
1183
|
-
|
|
1403
|
+
serialized.clear();
|
|
1404
|
+
pair.second.serialize_into(serialized);
|
|
1184
1405
|
fwrite(serialized.data(), 1, serialized.size(), file);
|
|
1185
1406
|
}
|
|
1186
1407
|
|