-
Notifications
You must be signed in to change notification settings - Fork 246
Expand file tree
/
Copy pathcluster_client.cpp
More file actions
2961 lines (2759 loc) · 154 KB
/
Copy pathcluster_client.cpp
File metadata and controls
2961 lines (2759 loc) · 154 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (C) 2011-2026 Redis Labs Ltd.
*
* This file is part of memtier_benchmark.
*
* memtier_benchmark is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 2.
*
* memtier_benchmark is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with memtier_benchmark. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_NETINET_TCP_H
#include <netinet/tcp.h>
#endif
#ifdef HAVE_LIMITS_H
#include <limits.h>
#endif
#ifdef HAVE_ASSERT_H
#include <assert.h>
#endif
#include <atomic>
#include <map>
#include <algorithm>
#include "cluster_client.h"
#include "command_meta.h"
#include "memtier_benchmark.h"
#include "obj_gen.h"
#include "retry_policy.h"
#include "shard_connection.h"
#define KEY_INDEX_QUEUE_MAX_SIZE 1000000
#define STAGED_MONITOR_QUEUE_MAX_SIZE 1000000
// After this many consecutive read-routing failures under strict rp_secondary
// (select_target_conn returning UINT_MAX for reads), hold_pipeline yields the
// event loop so the reactor can fire BEV_EVENT_CONNECTED for in-progress
// replica connections instead of busy-spinning through fill_pipeline.
#define STRICT_NO_ROUTE_HOLD_THRESHOLD 64
#define MOVED_MSG_PREFIX "-MOVED"
#define MOVED_MSG_PREFIX_LEN 6
#define ASK_MSG_PREFIX "-ASK"
#define ASK_MSG_PREFIX_LEN 4
#define MAX_CLUSTER_HSLOT 16383
static const uint16_t crc16tab[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad,
0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a,
0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b,
0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861,
0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96,
0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87,
0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a,
0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3,
0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290,
0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e,
0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f,
0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,
0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83,
0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74,
0x2e93, 0x3eb2, 0x0ed1, 0x1ef0};
static inline uint16_t crc16(const char *buf, size_t len)
{
size_t counter;
uint16_t crc = 0;
for (counter = 0; counter < len; counter++)
crc = (crc << 8) ^ crc16tab[((crc >> 8) ^ *buf++) & 0x00FF];
return crc;
}
static uint32_t calc_hslot_crc16_cluster(const char *str, size_t length)
{
uint32_t rv = (uint32_t) crc16(str, length) & MAX_CLUSTER_HSLOT;
return rv;
}
// Hash-tag-aware variant of calc_hslot_crc16_cluster. Mirrors Redis' rule
// (https://redis.io/docs/reference/cluster-spec/#hash-tags): if the key
// contains a {tag} substring with at least one byte between the braces, only
// the bytes inside the first such {tag} are hashed; otherwise the whole key
// is hashed. memtier's default slot computation skips this rule because the
// generic routing path only sees obj_gen->get_key() — never the literal
// affixes like "{mx}-" that the user wrote in --command. This helper is used
// by --transaction to pin to the actual slot owner.
static uint32_t calc_hslot_crc16_with_hash_tag(const char *str, size_t length)
{
const char *open = (const char *) memchr(str, '{', length);
if (open != NULL) {
size_t remaining = length - (open - str) - 1;
const char *close = (const char *) memchr(open + 1, '}', remaining);
if (close != NULL && close > open + 1) {
size_t tag_len = close - open - 1;
return (uint32_t) crc16(open + 1, tag_len) & MAX_CLUSTER_HSLOT;
}
}
return (uint32_t) crc16(str, length) & MAX_CLUSTER_HSLOT;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
cluster_client::cluster_client(client_group *group) :
client(group),
m_txn_pinned_conn_id(-1),
m_txn_observed_rotation_seq(0),
m_txn_staged_key_index(0),
m_txn_has_staged_key(false),
m_txn_round_key_index(0),
m_txn_round_key_valid(false),
m_txn_pin_lost_warned(false),
m_strict_no_route_attempts(0),
m_last_no_replica_warning_ts(0)
{
// Initialize slot map to the UINT_MAX sentinel; m_shard_groups starts empty
// and is populated by handle_cluster_slots(). Every reader bails on
// UINT_MAX (slot_primary_conn_id, select_target_conn, get_key_for_conn,
// retry_after_redirect, create_monitor_request_cluster, create_mget_request),
// so the pre-bootstrap window cannot silently route to a stale group index.
// cluster_client::connect() still forces the bootstrap CLUSTER SLOTS before
// user traffic flows; the sentinel is belt-and-braces against a topology
// refresh leaving stale entries for slots absent from the new reply.
m_slot_to_shard_group.assign(MAX_CLUSTER_HSLOT + 1, UINT_MAX);
}
cluster_client::~cluster_client()
{
for (unsigned int i = 0; i < m_key_index_pools.size(); i++) {
key_index_pool *key_idx_pool = m_key_index_pools[i];
delete key_idx_pool;
}
m_key_index_pools.clear();
}
int cluster_client::connect(void)
{
// get main connection
shard_connection *sc = MAIN_CONNECTION;
assert(sc != NULL);
// set main connection to send 'CLUSTER SLOTS' command
sc->set_cluster_slots();
// create key index pool for main connection only on the first connect.
// On reconnects (e.g. via --reconnect-on-error), the main connection's
// key_index_pool already exists and m_connections is unchanged, so
// pushing again would break the invariant the assertion enforces.
if (m_key_index_pools.empty()) {
key_index_pool *key_idx_pool = new key_index_pool;
m_key_index_pools.push_back(key_idx_pool);
m_staged_monitor_commands.emplace_back();
}
assert(m_connections.size() == m_key_index_pools.size());
assert(m_connections.size() == m_staged_monitor_commands.size());
// continue with base class
client::connect();
return 0;
}
void cluster_client::txn_release_pin()
{
if (m_txn_has_staged_key) {
// Return the staged key to the pin connection's pool so the next
// rotation can reuse it. Without this, every mid-rotation pin reset
// (MOVED, disconnect) silently burns one key from the sequential
// iterator, permanently skipping that index under --key-pattern=S.
m_key_index_pools[m_txn_pinned_conn_id]->push(m_txn_staged_key_index);
m_txn_has_staged_key = false;
}
// Forget the rotation's shared key: a dropped pin restarts the rotation on
// a fresh pin/lookahead, which will adopt a new key. Leaving this set would
// make the re-pinned rotation reuse the old key while the fresh lookahead
// key goes unconsumed -- burning a sequential index at the next wrap.
m_txn_round_key_valid = false;
m_txn_pinned_conn_id = -1;
}
void cluster_client::disconnect(void)
{
// Reset transaction pin state so a post-reconnect topology with fewer
// shards doesn't leave m_txn_pinned_conn_id pointing past the end of
// m_connections (which would be an out-of-bounds access).
m_txn_pinned_conn_id = -1;
m_txn_has_staged_key = false;
m_txn_round_key_valid = false;
m_txn_pin_lost_warned = false;
unsigned int conn_size = m_connections.size();
unsigned int i;
// disconnect all connections
for (i = 0; i < m_connections.size(); i++) {
shard_connection *sc = m_connections[i];
sc->disconnect();
}
// delete all connections except main connection
for (i = conn_size - 1; i > 0; i--) {
shard_connection *sc = m_connections.back();
m_connections.pop_back();
delete sc;
// m_key_index_pools and m_staged_monitor_commands are intentionally NOT shrunk:
// their entries are cleared by connect_shard_connection() on the next reconnect.
// Keeping them here avoids a size divergence between the two parallel vectors
// (which would fire the assert in create_shard_connection on re-connect).
}
}
shard_connection *cluster_client::create_shard_connection(abstract_protocol *abs_protocol)
{
shard_connection *sc = new shard_connection(m_connections.size(), this, m_config, m_event_base, abs_protocol);
assert(sc != NULL);
// The shard_connection ctor clones abs_protocol, and clone() resets runtime
// flags to defaults, so re-apply the arbitrary-command miss-tracking flag
// here. Without this, per-shard connections discovered after setup_client()
// never track misses and cluster-mode miss accounting is silently a no-op
// (issue #476).
apply_arbitrary_tracking_flags(sc->get_protocol());
m_connections.push_back(sc);
// create key index pool
key_index_pool *key_idx_pool = new key_index_pool;
assert(key_idx_pool != NULL);
m_key_index_pools.push_back(key_idx_pool);
m_staged_monitor_commands.emplace_back();
assert(m_connections.size() == m_key_index_pools.size());
assert(m_connections.size() == m_staged_monitor_commands.size());
return sc;
}
bool cluster_client::connect_shard_connection(shard_connection *sc, char *address, char *port)
{
// empty key index queue and staged monitor commands
if (m_key_index_pools[sc->get_id()]->size()) {
// Cross-shard reads that were routed to this connection (via
// create_request_for_other) push a (command_index, key_index) pair into
// m_key_index_pools[sc->get_id()] AFTER client::create_request has
// already incremented m_reqs_generated (client.cpp:656 for GET, the
// arbitrary path at create_request_for_other for --command). Clearing
// the pool here without compensating leaves m_reqs_processed < m_reqs_generated
// permanently, so a --requests run hangs and a --test-time run
// mis-accounts pending in-flight. Compensate by n/2 (pairs).
// Defensive clamp guards against underflow from any future single-entry
// push pattern (txn_release_pin pushes a lone key, but the pin path
// does not increment m_reqs_generated for that staged key -- it is
// consumed by the next rotation -- so n/2 is the right accounting
// for cross-shard residue and a no-op for the txn-staged singleton
// beyond the clamp.) Matches the pattern at hold_pipeline.
key_index_pool empty_queue;
std::swap(*m_key_index_pools[sc->get_id()], empty_queue);
{
const size_t n = empty_queue.size() / 2;
m_reqs_generated -= (m_reqs_generated >= n) ? n : m_reqs_generated;
}
}
{
std::queue<staged_monitor_cmd> empty_staged;
std::swap(m_staged_monitor_commands[sc->get_id()], empty_staged);
// Commands in the cleared staged queue were already counted in m_reqs_generated at
// staging time. Compensate so a --requests run is not left waiting for responses
// that will never arrive.
// Defensive clamp: today entries in empty_staged are popped before m_reqs_generated
// is decremented so underflow cannot occur, but guard explicitly so a future
// refactor does not silently wrap to 2^64. Matches the pattern at hold_pipeline.
{
const size_t n = empty_staged.size();
m_reqs_generated -= (m_reqs_generated >= n) ? n : m_reqs_generated;
}
}
// save address and port
sc->set_address_port(address, port);
// get address information
struct connect_info ci;
struct addrinfo *addr_info;
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC;
int res = getaddrinfo(address, port, &hints, &addr_info);
if (res != 0) {
benchmark_error_log("connect: resolve error: %s\n", gai_strerror(res));
return false;
}
ci.ci_family = addr_info->ai_family;
ci.ci_socktype = addr_info->ai_socktype;
ci.ci_protocol = addr_info->ai_protocol;
assert(addr_info->ai_addrlen <= sizeof(ci.addr_buf));
memcpy(ci.addr_buf, addr_info->ai_addr, addr_info->ai_addrlen);
ci.ci_addr = (struct sockaddr *) ci.addr_buf;
ci.ci_addrlen = addr_info->ai_addrlen;
freeaddrinfo(addr_info);
// call connect
res = sc->connect(&ci);
return res == 0;
}
void cluster_client::build_mget_slot_cache()
{
if (!m_config->multi_key_get) return;
mget_slot_cache *cache = m_config->mget_cache;
assert(cache != NULL);
unsigned int num_conns = (unsigned int) m_connections.size();
// Slot→key mapping is topology-independent: build it once across all threads.
pthread_mutex_lock(&cache->mutex);
if (!cache->built.load(std::memory_order_relaxed)) {
unsigned long long key_min = m_config->key_minimum;
unsigned long long key_max = m_config->key_maximum;
// Cap per-slot storage: multi_key_get * 4, bounded to [multi_key_get, 4096].
// This bounds both memory and scan time regardless of key range size.
unsigned int cap = (unsigned int) m_config->multi_key_get * 4;
if (cap > 4096) cap = 4096;
if (cap < (unsigned int) m_config->multi_key_get) cap = (unsigned int) m_config->multi_key_get;
benchmark_error_log("Building MGET slot cache for key range [%llu, %llu] "
"(cap %u keys/slot)...\n",
key_min, key_max, cap);
cache->slot_keys.assign(MAX_CLUSTER_HSLOT + 1, std::vector<unsigned long long>());
unsigned int filled_slots = 0;
for (unsigned long long idx = key_min; idx <= key_max && filled_slots < MAX_CLUSTER_HSLOT + 1; idx++) {
m_obj_gen->generate_key(idx);
unsigned int slot = calc_hslot_crc16_with_hash_tag(m_obj_gen->get_key(), m_obj_gen->get_key_len());
if (cache->slot_keys[slot].size() < cap) {
cache->slot_keys[slot].push_back(idx);
if (cache->slot_keys[slot].size() == cap) filled_slots++;
}
}
cache->built.store(true, std::memory_order_release);
// Count slots that ended up with at least one key (informational).
unsigned int populated = 0;
for (unsigned int s = 0; s <= MAX_CLUSTER_HSLOT; s++) {
if (!cache->slot_keys[s].empty()) populated++;
}
benchmark_error_log("MGET slot cache built: %u/%u slots populated.\n", populated, MAX_CLUSTER_HSLOT + 1);
}
pthread_mutex_unlock(&cache->mutex);
// Per-thread cursor: one entry per slot, sized to match the shared table.
m_mget_slot_cursor.assign(MAX_CLUSTER_HSLOT + 1, 0);
// Conn→slot mapping depends on topology: rebuild on every refresh.
m_mget_conn_slots.assign(num_conns, std::vector<unsigned int>());
m_mget_conn_slot_cursor.assign(num_conns, 0);
for (unsigned int slot = 0; slot <= MAX_CLUSTER_HSLOT; slot++) {
if (cache->slot_keys[slot].empty()) continue;
// Resolve slot -> shard_group -> primary -> conn_id. Behavior identical
// to the prior `m_slot_to_shard[slot]` lookup; the indirection through
// m_shard_groups is the prep for replica-aware routing.
unsigned int gidx = m_slot_to_shard_group[slot];
if (gidx >= m_shard_groups.size()) continue;
shard_connection *primary = m_shard_groups[gidx].primary;
if (primary == NULL) continue;
unsigned int cid = primary->get_id();
if (cid < num_conns) m_mget_conn_slots[cid].push_back(slot);
// Slot ownership is intentionally primary-only: exactly one producer
// per shard. The primary's fill_pipeline ticks drive create_mget_request,
// which then calls select_target_conn(slot, true) to honor
// --read-preference and dispatch the MGET to the chosen primary or
// replica connection. Attaching the same slot list to replica conns as
// well caused duplicate MGET issuance (the replica's own fill_pipeline
// would also produce an MGET for "its" slot, and m_get_ratio_count —
// shared at the client level — would advance twice per cycle, so the
// configured --ratio no longer matched on-the-wire request counts).
// Replica conns with an empty slot list take the "exhausted" branch
// in create_mget_request, which resets the SET/GET cycle so mixed
// --ratio workloads keep producing SETs (routed to the primary) from
// every connection.
}
}
unsigned int cluster_client::slot_primary_conn_id(unsigned int slot) const
{
if (slot >= m_slot_to_shard_group.size()) return UINT_MAX;
unsigned int gidx = m_slot_to_shard_group[slot];
if (gidx >= m_shard_groups.size()) return UINT_MAX;
shard_connection *primary = m_shard_groups[gidx].primary;
if (primary == NULL) return UINT_MAX;
return primary->get_id();
}
// True if `sc` is in a usable steady state for sending a fresh user-level
// request: TCP connected, cluster-slots ladder finished, and (for replicas)
// the READONLY ladder also finished. Delegates to shard_connection::
// is_ready_for_reads() which encodes the full per-role readiness predicate.
static inline bool conn_is_live_for_routing(shard_connection *sc)
{
if (sc == NULL) return false;
return sc->is_ready_for_reads();
}
unsigned int cluster_client::select_target_conn(unsigned int slot, bool is_read)
{
if (slot >= m_slot_to_shard_group.size()) return UINT_MAX;
unsigned int gidx = m_slot_to_shard_group[slot];
if (gidx >= m_shard_groups.size()) return UINT_MAX;
shard_group &group = m_shard_groups[gidx];
if (group.primary == NULL) return UINT_MAX;
// Writes always go to the primary. This is the only sane choice; replicas
// either reject (-READONLY) or accept and immediately diverge from the
// authoritative primary.
//
// Writes only require the primary's TCP socket to be connected; the full
// is_ready_for_reads() predicate (which also requires
// m_cluster_slots == setup_done) is unnecessarily strict here. Several
// code paths transiently flip the primary's m_cluster_slots back to
// setup_none (role-aware disconnect, MOVED redirect, READONLY-no-loop
// guard, the build-then-swap window of a CLUSTER SLOTS refresh). During
// those windows is_ready_for_reads() returns false even though the slot
// map is still valid and dispatching a SET on the producer's own slot
// will not loop. Because client::get_key_for_conn has already advanced
// m_obj_gen->m_next_key by the time we get here, returning UINT_MAX
// *consumes* the key index without issuing the SET -- and when the
// iterator wraps at m_key_max the client re-writes already-touched
// keys, manifesting as a deterministic per-run key shortfall
// (observed pattern: ~9 skips x 20 clients = ~176 keys; 500000 == 499824).
//
// The cluster_slots gate is meaningful only for reads, which need a
// valid slot map to ROUTE; writes to the producer's primary do not
// re-look-up routing. Reads continue to use the full
// conn_is_live_for_routing() / is_ready_for_reads() predicate below.
//
// Round-13: even the conn_connected gate has been removed for writes.
// The producer's own slot is conn_connected by definition (fill_pipeline
// runs only on connected conns). For cross-shard writes, the target's
// own fill_pipeline waits for setup_done before draining its pool, so
// queueing here is safe regardless of target TCP state. The pre-PR
// master path returned available_for_conn unconditionally for
// producer==primary; this restores that invariant for both
// producer-owned and cross-shard writes. Gating on transient
// conn_in_progress/conn_disconnected (bootstrap or post-reconnect)
// silently consumed the key index advanced by client::get_key_for_conn
// and was the root cause of the 500000 == 499824 keyspace loss.
if (!is_read) {
return group.primary->get_id();
}
const enum read_pref_mode mode = m_config->read_preference;
// Used by both rp_nearest (cold-seed RR) and rp_secondary/_preferred (live
// replica RR). Hoisted above the mode switch so the two branches share one
// declaration instead of shadowing each other.
const size_t nreplicas = group.replicas.size();
// rp_primary: legacy behavior (read from primary). Falling all the way
// through to the slot's primary owner gives parity with the
// pre-read-preference world.
if (mode == rp_primary) {
return conn_is_live_for_routing(group.primary) ? group.primary->get_id() : UINT_MAX;
}
// rp_nearest: scan warm replicas (and the primary if it has already
// accumulated samples) and pick the lowest EWMA. Cold (samples <
// threshold) entries are treated as +inf so the tiebreak is among warm
// endpoints. While any live replica is still cold, route to it round-
// robin so every replica accumulates its first LATENCY_EWMA_MIN_SAMPLES;
// once all live replicas are warm, the EWMA pick below takes over.
// (Consider primary first - under built-in GET/MGET workloads it never
// warms because no rt_get response reaches it, but under arbitrary
// mixed workloads rt_arbitrary writes update its EWMA, so it can
// participate in selection. The no-live-replica fallback below also
// routes traffic to primary, which independently warms it.)
if (mode == rp_nearest) {
shard_connection *best = NULL;
double best_ewma = 0.0;
// Consider primary first (contends only if it has accumulated
// samples via rt_arbitrary writes or the no-live-replica fallback;
// see comment above).
if (conn_is_live_for_routing(group.primary) && group.primary->latency_ewma_warm()) {
best = group.primary;
best_ewma = group.primary->get_latency_ewma_us();
}
bool any_cold_live_replica = false;
for (size_t i = 0; i < nreplicas; i++) {
shard_connection *r = group.replicas[i];
if (!conn_is_live_for_routing(r)) continue;
if (!r->latency_ewma_warm()) {
any_cold_live_replica = true;
continue;
}
const double e = r->get_latency_ewma_us();
if (best == NULL || e < best_ewma) {
best = r;
best_ewma = e;
}
}
// Seed cold live replicas first: round-robin over the replica list
// looking for any live-but-not-yet-warm replica. Once everyone is
// warm, this loop finds nothing and we fall through to either the
// warm pick (`best`) or the primary fallback.
if (any_cold_live_replica && nreplicas > 0) {
for (size_t step = 0; step < nreplicas; step++) {
unsigned int idx = (group.replica_rr_cursor + step) % nreplicas;
shard_connection *r = group.replicas[idx];
if (conn_is_live_for_routing(r) && !r->latency_ewma_warm()) {
group.replica_rr_cursor = (idx + 1) % nreplicas;
return r->get_id();
}
}
}
if (best != NULL) return best->get_id();
// No warm endpoint and no cold live replica — keep traffic on the
// primary so reads still flow during the warm-up window.
return conn_is_live_for_routing(group.primary) ? group.primary->get_id() : UINT_MAX;
}
// rp_secondary / rp_secondary_preferred: round-robin over live replicas.
// The cursor lives on the per-thread shard_group and wraps; we walk up
// to N entries so a single dead replica doesn't push us back to primary.
if (nreplicas > 0) {
for (size_t step = 0; step < nreplicas; step++) {
unsigned int idx = (group.replica_rr_cursor + step) % nreplicas;
shard_connection *r = group.replicas[idx];
if (conn_is_live_for_routing(r)) {
group.replica_rr_cursor = (idx + 1) % nreplicas;
return r->get_id();
}
}
}
// No live replica. rp_secondary_preferred falls back to the primary;
// rp_secondary returns UINT_MAX and lets the caller apply
// --read-preference-fallback (rpf_error / rpf_queue / rpf_primary).
if (mode == rp_secondary_preferred) {
return conn_is_live_for_routing(group.primary) ? group.primary->get_id() : UINT_MAX;
}
// Strict rp_secondary: honor --read-preference-fallback at the routing
// site. rpf_primary silently degrades to the primary; rpf_error and
// rpf_queue both return UINT_MAX (caller treats as not_available).
if (m_config->read_preference_fallback == rpf_primary) {
return conn_is_live_for_routing(group.primary) ? group.primary->get_id() : UINT_MAX;
}
return UINT_MAX;
}
bool cluster_client::classify_read(const request *req) const
{
if (req == NULL) return false;
switch (req->m_type) {
case rt_get:
return true;
case rt_set:
case rt_wait: // WAIT must run on the primary; classify as write
case rt_auth:
case rt_select_db:
case rt_cluster_slots:
case rt_hello:
case rt_readonly:
return false;
case rt_arbitrary: {
const arbitrary_request *ar = static_cast<const arbitrary_request *>(req);
if (ar->m_cmd_meta == NULL) return false;
// Per-command override takes precedence over the command-meta lookup.
if (ar->m_cmd_meta->is_read_override == 1) return true;
if (ar->m_cmd_meta->is_read_override == 0) return false;
// Spec-resolved is_read flag (READONLY in Redis command-flags).
if (ar->m_cmd_meta->spec != NULL) return ar->m_cmd_meta->spec->is_read;
return false;
}
case rt_unknown:
default:
return false;
}
}
void cluster_client::record_read_routing(size_t arbitrary_index, bool from_replica)
{
if (m_arbitrary_routing_counters.empty()) {
// Lazily size to match the configured --command count. handle_response
// can land here before run_stats finishes its own per-command setup,
// so size on first use.
if (m_config && m_config->arbitrary_commands) {
m_arbitrary_routing_counters.assign(m_config->arbitrary_commands->size(), read_routing_counters());
}
}
if (arbitrary_index >= m_arbitrary_routing_counters.size()) return;
if (from_replica)
m_arbitrary_routing_counters[arbitrary_index].ops_from_replica++;
else
m_arbitrary_routing_counters[arbitrary_index].ops_from_primary++;
}
void cluster_client::record_builtin_read_routing(request_type rt, bool from_replica)
{
if (rt != rt_get) return;
if (from_replica)
m_get_routing_counters.ops_from_replica++;
else
m_get_routing_counters.ops_from_primary++;
}
// "host:port" for a shard_connection, defensive against NULL fields.
static std::string sc_endpoint_addr(shard_connection *sc)
{
const char *a = sc->get_address();
const char *p = sc->get_port();
std::string s(a ? a : "?");
s += ":";
s += (p ? p : "?");
return s;
}
// Sort [start,end] ranges and merge touching/overlapping ones into the minimal
// set of contiguous ranges.
static std::vector<std::pair<int, int> > merge_slot_ranges(std::vector<std::pair<int, int> > v)
{
std::sort(v.begin(), v.end());
std::vector<std::pair<int, int> > m;
for (size_t i = 0; i < v.size(); i++) {
if (!m.empty() && v[i].first <= m.back().second + 1)
m.back().second = std::max(m.back().second, v[i].second);
else
m.push_back(v[i]);
}
return m;
}
std::vector<cluster_endpoint_info> cluster_client::build_topology_snapshot() const
{
std::vector<cluster_endpoint_info> out;
if (m_shard_groups.empty()) return out;
// Reconstruct each group's contiguous slot runs by scanning the slot map
// once. A group == one CLUSTER SLOTS shard entry (one contiguous range).
std::vector<std::vector<std::pair<int, int> > > group_ranges(m_shard_groups.size());
unsigned int run_group = UINT_MAX;
int run_start = 0;
for (int s = 0; s <= MAX_CLUSTER_HSLOT; s++) {
unsigned int g = (s < (int) m_slot_to_shard_group.size()) ? m_slot_to_shard_group[s] : UINT_MAX;
if (g != run_group) {
if (run_group != UINT_MAX && run_group < group_ranges.size())
group_ranges[run_group].push_back(std::make_pair(run_start, s - 1));
run_group = g;
run_start = s;
}
}
if (run_group != UINT_MAX && run_group < group_ranges.size())
group_ranges[run_group].push_back(std::make_pair(run_start, MAX_CLUSTER_HSLOT));
// Coalesce by distinct endpoint (host:port). A single node commonly owns
// many disjoint slot ranges (striped layouts), each a separate shard_group
// that points at the SAME shard_connection; the client opens ONE connection
// per node, so the topology must be counted by node, not by group. Merge
// every group's ranges into its node's entry and recompute contiguous runs.
// First-seen order is preserved for stable output.
std::vector<std::string> primary_order;
std::map<std::string, std::vector<std::pair<int, int> > > primary_ranges;
std::vector<std::string> replica_order;
std::map<std::string, std::string> replica_primary; // replica addr -> its primary addr
std::map<std::string, std::vector<std::pair<int, int> > > replica_ranges;
for (size_t gi = 0; gi < m_shard_groups.size(); gi++) {
const shard_group &grp = m_shard_groups[gi];
if (grp.primary == NULL) continue;
std::string paddr = sc_endpoint_addr(grp.primary);
if (primary_ranges.find(paddr) == primary_ranges.end()) primary_order.push_back(paddr);
std::vector<std::pair<int, int> > &pr = primary_ranges[paddr];
pr.insert(pr.end(), group_ranges[gi].begin(), group_ranges[gi].end());
for (size_t ri = 0; ri < grp.replicas.size(); ri++) {
shard_connection *rep = grp.replicas[ri];
if (rep == NULL) continue;
std::string raddr = sc_endpoint_addr(rep);
if (replica_ranges.find(raddr) == replica_ranges.end()) {
replica_order.push_back(raddr);
replica_primary[raddr] = paddr;
}
std::vector<std::pair<int, int> > &rr = replica_ranges[raddr];
rr.insert(rr.end(), group_ranges[gi].begin(), group_ranges[gi].end());
}
}
// Primaries first (first-seen order), then replicas.
for (size_t i = 0; i < primary_order.size(); i++) {
cluster_endpoint_info info;
info.addr = primary_order[i];
info.is_primary = true;
info.slot_ranges = merge_slot_ranges(primary_ranges[primary_order[i]]);
out.push_back(info);
}
for (size_t i = 0; i < replica_order.size(); i++) {
cluster_endpoint_info info;
info.addr = replica_order[i];
info.is_primary = false;
info.primary_addr = replica_primary[replica_order[i]];
info.slot_ranges = merge_slot_ranges(replica_ranges[replica_order[i]]);
out.push_back(info);
}
return out;
}
void cluster_client::print_topology_summary() const
{
std::vector<cluster_endpoint_info> eps = build_topology_snapshot();
if (eps.empty()) return;
// Signature of the topology shape (endpoints + roles + slot ranges). The
// startup block prints on the first commit of each run AND reprints when
// this signature changes within a run, so a mid-run CLUSTER SLOTS refresh
// that alters the topology is logged and the stderr output stays consistent
// with the run-end results summary (which snapshots the final topology).
unsigned long long sig = 1469598103934665603ULL; // FNV-1a 64-bit offset basis
for (size_t i = 0; i < eps.size(); i++) {
const cluster_endpoint_info &e = eps[i];
for (size_t c = 0; c < e.addr.size(); c++) {
sig ^= (unsigned char) e.addr[c];
sig *= 1099511628211ULL;
}
sig ^= (e.is_primary ? 0x9E3779B97F4A7C15ULL : 0xC2B2AE3D27D4EB4FULL);
sig *= 1099511628211ULL;
for (size_t r = 0; r < e.slot_ranges.size(); r++) {
sig ^= (unsigned long long) e.slot_ranges[r].first;
sig *= 1099511628211ULL;
sig ^= (unsigned long long) e.slot_ranges[r].second;
sig *= 1099511628211ULL;
}
}
// Process-global gate keyed on (run id, signature). Guarded by a mutex
// because the signature is wider than a lock-free atomic and several worker
// threads can reach this concurrently after a refresh. The lock is held
// across the whole emit (not just the gate update) so two workers cannot
// interleave their blocks or print a superseded topology after a newer one.
const unsigned int rid = m_config->current_run_id;
static pthread_mutex_t s_mtx = PTHREAD_MUTEX_INITIALIZER;
static unsigned int s_last_run = UINT_MAX;
static unsigned long long s_last_sig = 0;
pthread_mutex_lock(&s_mtx);
if (s_last_run == rid && s_last_sig == sig) {
pthread_mutex_unlock(&s_mtx);
return;
}
s_last_run = rid;
s_last_sig = sig;
size_t primaries = 0, replicas = 0;
for (size_t i = 0; i < eps.size(); i++) {
if (eps[i].is_primary)
primaries++;
else
replicas++;
}
if (replicas > 0)
fprintf(stderr, "[RUN #%u] Cluster topology: %zu endpoints discovered (%zu primaries, %zu replicas)\n", rid,
eps.size(), primaries, replicas);
else
fprintf(stderr, "[RUN #%u] Cluster topology: %zu endpoints discovered (%zu primaries)\n", rid, eps.size(),
primaries);
// One compact line per distinct node: slot count + range count (not the
// enumerated ranges -- a striped node can own dozens). Cap the list so a
// large cluster doesn't flood the log; the counts above are authoritative.
const size_t LIST_CAP = 40;
const size_t shown = eps.size() < LIST_CAP ? eps.size() : LIST_CAP;
for (size_t i = 0; i < shown; i++) {
const cluster_endpoint_info &e = eps[i];
size_t slots = 0;
for (size_t r = 0; r < e.slot_ranges.size(); r++)
slots += (size_t) (e.slot_ranges[r].second - e.slot_ranges[r].first + 1);
const size_t nr = e.slot_ranges.size();
const char *rp = (nr == 1) ? "" : "s";
const char *sp = (slots == 1) ? "" : "s";
if (e.is_primary)
fprintf(stderr, " %-22s primary %5zu slot%s in %zu range%s\n", e.addr.c_str(), slots, sp, nr,
rp);
else
fprintf(stderr, " %-22s replica %5zu slot%s in %zu range%s -> %s\n", e.addr.c_str(), slots, sp,
nr, rp, e.primary_addr.c_str());
}
if (eps.size() > shown) fprintf(stderr, " ... and %zu more endpoints\n", eps.size() - shown);
// Total opened connections = threads x conns-per-thread x endpoints. Each
// cluster_client opens one shard_connection per distinct endpoint, so the
// endpoint count is the per-client connection fan-out. With the
// --clients-start staircase ramp, m_config->clients is the peak (full-ramp)
// value, so the figure is the target fan-out; flag that so it isn't read as
// the count already opened.
const char *ramp = (m_config->clients_start > 0) ? " at full ramp" : "";
const unsigned long long endpoints = (unsigned long long) eps.size();
const unsigned long long total_conns =
(unsigned long long) m_config->threads * (unsigned long long) m_config->clients * endpoints;
fprintf(stderr, "[RUN #%u] Total connections%s: %u threads x %u conns/thread x %llu endpoints = %llu\n", rid, ramp,
m_config->threads, m_config->clients, endpoints, total_conns);
// --rate-limiting is per-connection; surface the aggregate target only when
// set. Count only request-generating connections: primaries always, plus
// replicas only when reads route to them (otherwise replica connections sit
// idle and never hit their per-connection limiter).
if (m_config->request_rate > 0) {
const unsigned long long traffic_eps =
(unsigned long long) primaries +
(m_config->read_preference != rp_primary ? (unsigned long long) replicas : 0);
const unsigned long long aggregate = (unsigned long long) m_config->request_rate *
(unsigned long long) m_config->threads *
(unsigned long long) m_config->clients * traffic_eps;
fprintf(stderr, "[RUN #%u] Rate limit: %u req/s/conn -> %llu req/s aggregate target%s\n", rid,
m_config->request_rate, aggregate, ramp);
}
pthread_mutex_unlock(&s_mtx);
}
bool cluster_client::handle_cluster_slots(protocol_response *r)
{
/*
* temporary array to test if some of the connections are left with no
* slots, and need to be closed.
*/
unsigned long prev_connections_size = m_connections.size();
std::vector<bool> close_sc(prev_connections_size, true);
// Validate the top-level reply shape before walking it. as_mbulk_size()
// and as_bulk() both call assert(0) on type mismatch, and bare
// mbulks_elements[N] indexing is UB past-end. A malformed CLUSTER SLOTS
// reply from a misbehaving / hostile server (#417: fixture
// `cluster_slots_malformed.bin` from #409) hit both. Drop malformed
// shards instead of crashing; if the entire reply is unusable, the
// existing bootstrap connection stays in service.
if (r->get_mbulk_value() == NULL) {
benchmark_error_log("warning: CLUSTER SLOTS: server returned non-array; ignoring reply\n");
return false;
}
// A *valid* zero-shard reply would silently retire every existing
// connection (the close_sc[] loop further down). That's worse than
// crashing -- the benchmark continues with no shards. Reject it.
if (r->get_mbulk_value()->mbulks_elements.size() == 0) {
benchmark_error_log("warning: CLUSTER SLOTS: server returned empty topology; ignoring reply\n");
return false;
}
// Track whether any shard in the reply passed validation. If every shard
// is malformed and we fall through the loop with no `close_sc[j] = false`
// anywhere, the close-stale-connections pass below would tear down EVERY
// existing connection (including the bootstrap), which contradicts the
// documented "bootstrap stays in service" invariant. (Cursor bugbot.)
bool any_valid_shard = false;
// Build the new topology into LOCAL buffers (build-then-swap pattern).
//
// Parse into new_slot_map / new_groups locals. Only swap into the member
// variables if at least one valid shard was produced. On an all-skipped
// reply the member state is untouched and a warning is emitted, so traffic
// continues to route against the prior topology rather than seeing an
// empty slot map until the next successful refresh. The stale-index
// aliasing invariant (slots absent from the new reply must map to
// UINT_MAX, not to a reused group index) is preserved because
// new_slot_map is initialised to UINT_MAX and is only written for slots
// explicitly listed in a valid shard range -- identical to an
// unconditional assign(), just deferred until commit time.
//
// Connections (m_connections) are still created/reused in-place during the
// parse loop; in the all-malformed case no new connections are created
// (every shard is skipped before reaching create_shard_connection), so
// there is nothing to roll back on that side.
assert(m_slot_to_shard_group.size() == (size_t) MAX_CLUSTER_HSLOT + 1);
std::vector<unsigned int> new_slot_map(MAX_CLUSTER_HSLOT + 1, UINT_MAX);
std::vector<shard_group> new_groups;
// Pre-scan all shard tuples to collect the set of (addr, port) endpoints
// advertised as a PRIMARY anywhere in this reply. The replica-registration
// loop below already guards against flipping a node that's a primary in
// an EARLIER or the CURRENT shard (via new_groups + `sc`), but it cannot
// see LATER shards that haven't been parsed yet. Without this pre-scan a
// node that serves as replica in shard N and primary in shard M > N would
// still get role_replica + rearm_readonly() applied at the replica loop,
// and the role flip would silently win until shard M is processed (which
// re-sets role_primary at line 803 but does NOT undo rearm_readonly).
// The known-gap comment at lines 921-929 documented exactly this.
// Bugbot HIGH iter6-R3 / round-29 cursor[bot] thread.
//
// Shape this as a vector<pair<string,string>> rather than a set so a
// malformed shard tuple (caught by the validation below) can still
// contribute defensively; we only consult contains(), and false negatives
// are harmless because the existing guards still cover earlier shards.
std::vector<std::pair<std::string, std::string> > advertised_primary_endpoints;
for (unsigned int pi = 0; pi < r->get_mbulk_value()->mbulks_elements.size(); pi++) {
mbulk_element *ps_el = r->get_mbulk_value()->mbulks_elements[pi];
if (ps_el == NULL || !ps_el->is_mbulk_size()) continue;
mbulk_size_el *ps = ps_el->as_mbulk_size();
if (ps->mbulks_elements.size() < 3 || !ps->mbulks_elements[2]->is_mbulk_size()) continue;
mbulk_size_el *pn = ps->mbulks_elements[2]->as_mbulk_size();
if (pn->mbulks_elements.size() < 2 || !pn->mbulks_elements[0]->is_bulk() || !pn->mbulks_elements[1]->is_bulk())
continue;
bulk_el *pa = pn->mbulks_elements[0]->as_bulk();
bulk_el *pp = pn->mbulks_elements[1]->as_bulk();
if (pa->value_len == 0 || pp->value_len == 0) continue;
if (memchr(pa->value, '\0', pa->value_len) != NULL) continue;
// pp->value points at the bulk header (':' integer-reply prefix);
// value+1 / value_len-1 strips it, matching the primary-port copy
// at lines 754-757.
std::string p_addr((const char *) pa->value, pa->value_len);
std::string p_port((const char *) pp->value + 1, pp->value_len - 1);
advertised_primary_endpoints.push_back(std::make_pair(p_addr, p_port));
}
// run over response and create connections
for (unsigned int i = 0; i < r->get_mbulk_value()->mbulks_elements.size(); i++) {
mbulk_element *shard_el = r->get_mbulk_value()->mbulks_elements[i];
if (shard_el == NULL || !shard_el->is_mbulk_size()) {
benchmark_error_log("warning: CLUSTER SLOTS: shard %u not an array; skipping\n", i);
continue;
}
mbulk_size_el *shard = shard_el->as_mbulk_size();
if (shard->mbulks_elements.size() < 3 || !shard->mbulks_elements[0]->is_bulk() ||
!shard->mbulks_elements[1]->is_bulk() || !shard->mbulks_elements[2]->is_mbulk_size()) {
benchmark_error_log(
"warning: CLUSTER SLOTS: shard %u malformed (need [start, end, [host, port, ...]]); skipping\n", i);
continue;
}
// Slot bounds: must be parseable, in [0, MAX_CLUSTER_HSLOT], and
// min <= max. The old code took the strtol result verbatim and
// wrote into m_slot_to_shard[min..max] -- a hostile server could
// make us write past the end of the 16384-sized array (OOB write).
bulk_el *min_el = shard->mbulks_elements[0]->as_bulk();
bulk_el *max_el = shard->mbulks_elements[1]->as_bulk();
if (min_el->value_len == 0 || max_el->value_len == 0) {
benchmark_error_log("warning: CLUSTER SLOTS: shard %u empty slot bound; skipping\n", i);
continue;
}
errno = 0;
long parsed_min = strtol(min_el->value + 1, NULL, 10);
long parsed_max = strtol(max_el->value + 1, NULL, 10);
if (errno == ERANGE || parsed_min < 0 || parsed_max < 0 || parsed_min > MAX_CLUSTER_HSLOT ||
parsed_max > MAX_CLUSTER_HSLOT || parsed_min > parsed_max) {
benchmark_error_log("warning: CLUSTER SLOTS: shard %u slot range [%ld, %ld] out of [0, %d]; skipping\n", i,
parsed_min, parsed_max, MAX_CLUSTER_HSLOT);
continue;
}
int min_slot = (int) parsed_min;
int max_slot = (int) parsed_max;
mbulk_size_el *node = shard->mbulks_elements[2]->as_mbulk_size();
if (node->mbulks_elements.size() < 2 || !node->mbulks_elements[0]->is_bulk() ||
!node->mbulks_elements[1]->is_bulk()) {
benchmark_error_log("warning: CLUSTER SLOTS: shard %u node tuple malformed (need host, port); skipping\n",
i);
continue;
}
// hostname/ip + port: reject zero-length bulks (memcpy(..., NULL+1, 0)
// is technically UB; embedded NULs would also alias other addrs in
// strcmp-based lookup).
bulk_el *mbulk_addr_el = node->mbulks_elements[0]->as_bulk();
bulk_el *mbulk_port_el = node->mbulks_elements[1]->as_bulk();