@@ -279,6 +279,68 @@ bool cluster_client::connect_shard_connection(shard_connection *sc, char *addres
279279 return res == 0 ;
280280}
281281
282+ void cluster_client::build_mget_slot_cache ()
283+ {
284+ if (!m_config->multi_key_get ) return ;
285+
286+ mget_slot_cache *cache = m_config->mget_cache ;
287+ assert (cache != NULL );
288+
289+ unsigned int num_conns = (unsigned int ) m_connections.size ();
290+
291+ // Slot→key mapping is topology-independent: build it once across all threads.
292+ pthread_mutex_lock (&cache->mutex );
293+ if (!cache->built .load (std::memory_order_relaxed)) {
294+ unsigned long long key_min = m_config->key_minimum ;
295+ unsigned long long key_max = m_config->key_maximum ;
296+
297+ // Cap per-slot storage: multi_key_get * 4, bounded to [multi_key_get, 4096].
298+ // This bounds both memory and scan time regardless of key range size.
299+ unsigned int cap = (unsigned int ) m_config->multi_key_get * 4 ;
300+ if (cap > 4096 ) cap = 4096 ;
301+ if (cap < (unsigned int ) m_config->multi_key_get ) cap = (unsigned int ) m_config->multi_key_get ;
302+
303+ benchmark_error_log (" Building MGET slot cache for key range [%llu, %llu] "
304+ " (cap %u keys/slot)...\n " ,
305+ key_min, key_max, cap);
306+
307+ cache->slot_keys .assign (MAX_CLUSTER_HSLOT + 1 , std::vector<unsigned long long >());
308+
309+ unsigned int filled_slots = 0 ;
310+ for (unsigned long long idx = key_min; idx <= key_max && filled_slots < MAX_CLUSTER_HSLOT + 1 ; idx++) {
311+ m_obj_gen->generate_key (idx);
312+ unsigned int slot = calc_hslot_crc16_with_hash_tag (m_obj_gen->get_key (), m_obj_gen->get_key_len ());
313+ if (cache->slot_keys [slot].size () < cap) {
314+ cache->slot_keys [slot].push_back (idx);
315+ if (cache->slot_keys [slot].size () == cap) filled_slots++;
316+ }
317+ }
318+
319+ cache->built .store (true , std::memory_order_release);
320+
321+ // Count slots that ended up with at least one key (informational).
322+ unsigned int populated = 0 ;
323+ for (unsigned int s = 0 ; s <= MAX_CLUSTER_HSLOT; s++) {
324+ if (!cache->slot_keys [s].empty ()) populated++;
325+ }
326+ benchmark_error_log (" MGET slot cache built: %u/%u slots populated.\n " , populated, MAX_CLUSTER_HSLOT + 1 );
327+ }
328+ pthread_mutex_unlock (&cache->mutex );
329+
330+ // Per-thread cursor: one entry per slot, sized to match the shared table.
331+ m_mget_slot_cursor.assign (MAX_CLUSTER_HSLOT + 1 , 0 );
332+
333+ // Conn→slot mapping depends on topology: rebuild on every refresh.
334+ m_mget_conn_slots.assign (num_conns, std::vector<unsigned int >());
335+ m_mget_conn_slot_cursor.assign (num_conns, 0 );
336+
337+ for (unsigned int slot = 0 ; slot <= MAX_CLUSTER_HSLOT; slot++) {
338+ if (cache->slot_keys [slot].empty ()) continue ;
339+ unsigned int cid = m_slot_to_shard[slot];
340+ if (cid < num_conns) m_mget_conn_slots[cid].push_back (slot);
341+ }
342+ }
343+
282344void cluster_client::handle_cluster_slots (protocol_response *r)
283345{
284346 /*
@@ -362,6 +424,19 @@ void cluster_client::handle_cluster_slots(protocol_response *r)
362424 }
363425 }
364426 }
427+
428+ // Rebuild same-slot key index cache for MGET if enabled.
429+ build_mget_slot_cache ();
430+
431+ // Wake all connected shard connections so each one re-evaluates hold_pipeline()
432+ // with the freshly-built m_mget_conn_slots. Without this, a connection that
433+ // was bufferevent_disable()'d before the cache existed would never re-run
434+ // fill_pipeline() and would stay permanently idle.
435+ if (m_config->multi_key_get > 0 ) {
436+ for (size_t i = 0 ; i < m_connections.size (); i++) {
437+ if (m_connections[i]->get_connection_state () != conn_disconnected) m_connections[i]->schedule_fill ();
438+ }
439+ }
365440}
366441
367442bool cluster_client::hold_pipeline (unsigned int conn_id)
@@ -392,6 +467,17 @@ bool cluster_client::hold_pipeline(unsigned int conn_id)
392467 }
393468 }
394469
470+ /* In GET-only MGET mode, a connection whose slots own no keys in the
471+ * configured key range can never generate a request. Returning true here
472+ * breaks the fill_pipeline while-loop for that connection so it does not
473+ * spin consuming CPU. Other connections (which do have eligible slots)
474+ * continue to operate normally. */
475+ if (m_config->multi_key_get > 0 && m_config->ratio .a == 0 && m_config->mget_cache != NULL &&
476+ m_config->mget_cache ->built .load (std::memory_order_acquire) && conn_id < m_mget_conn_slots.size () &&
477+ m_mget_conn_slots[conn_id].empty () && m_staged_monitor_commands[conn_id].empty ()) {
478+ return true ;
479+ }
480+
395481 /* In transaction mode the pin connection drives the entire rotation.
396482 * Non-pin connections must not spin in fill_pipeline; they will be
397483 * rescheduled via schedule_fill() when the pin is cleared. If the pin
@@ -649,6 +735,42 @@ bool cluster_client::create_arbitrary_request(unsigned int command_index, struct
649735 return true ;
650736}
651737
738+ bool cluster_client::create_mget_request (struct timeval ×tamp, unsigned int conn_id)
739+ {
740+ // Only reached when --multi-key-get is set.
741+ // Use the pre-built slot cache so all N keys in this MGET share one hash
742+ // slot — Redis requires exact same-slot (not just same-node) for MGET in
743+ // cluster mode. Cache is rebuilt on every topology change via
744+ // build_mget_slot_cache() at the end of handle_cluster_slots().
745+ unsigned int keys_count = m_config->ratio .b - m_get_ratio_count;
746+ if ((int ) keys_count > m_config->multi_key_get ) keys_count = m_config->multi_key_get ;
747+ if (keys_count == 0 ) return false ;
748+
749+ if (conn_id >= m_mget_conn_slots.size () || m_mget_conn_slots[conn_id].empty ()) {
750+ // Cache not ready or no key in the configured range maps to this shard.
751+ return false ;
752+ }
753+
754+ // Round-robin over the slots owned by this connection.
755+ size_t &sc = m_mget_conn_slot_cursor[conn_id];
756+ unsigned int target_slot = m_mget_conn_slots[conn_id][sc % m_mget_conn_slots[conn_id].size ()];
757+ sc++;
758+
759+ std::vector<unsigned long long > &slot_keys = m_config->mget_cache ->slot_keys [target_slot];
760+ size_t &kc = m_mget_slot_cursor[target_slot];
761+
762+ m_keylist->clear ();
763+ for (unsigned int i = 0 ; i < keys_count; i++) {
764+ unsigned long long idx = slot_keys[kc % slot_keys.size ()];
765+ kc++;
766+ m_obj_gen->generate_key (idx);
767+ m_keylist->add_key (m_obj_gen->get_key (), m_obj_gen->get_key_len ());
768+ }
769+
770+ m_connections[conn_id]->send_mget_command (×tamp, m_keylist);
771+ return true ;
772+ }
773+
652774void cluster_client::create_request (struct timeval timestamp, unsigned int conn_id)
653775{
654776 /* Drain staged monitor commands that were routed here from another shard connection. */
0 commit comments