Skip to content

Commit 51558dd

Browse files
Merge pull request #440 from QueryaHub/issue/426-redis-pipeline-type-ttl
perf(redis): pipeline TYPE/TTL for SCAN batches (#426)
2 parents cf13479 + 6c510bf commit 51558dd

3 files changed

Lines changed: 100 additions & 11 deletions

File tree

lib/core/database/redis_connection.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,45 @@ class RedisConnection {
206206
return result is int ? result : int.tryParse(result.toString()) ?? -1;
207207
}
208208

209+
/// Pipelined TYPE + TTL for a SCAN batch.
210+
///
211+
/// Writes all commands before awaiting replies (redis-dart FIFO parse
212+
/// queue + optional Nagle via [Command.pipe_start]), so a batch of N keys
213+
/// costs ~1 RTT instead of ~2N sequential round-trips.
214+
Future<List<({String type, int ttl})>> typesAndTtls(List<String> keys) async {
215+
if (keys.isEmpty) return const [];
216+
if (!isConnected) {
217+
throw StateError('Not connected to Redis');
218+
}
219+
220+
final cmd = _command;
221+
cmd?.pipe_start();
222+
try {
223+
final typeFutures = <Future<String>>[
224+
for (final key in keys)
225+
sendCommand(['TYPE', key]).then(
226+
(v) => v?.toString() ?? 'none',
227+
onError: (_) => 'unknown',
228+
),
229+
];
230+
final ttlFutures = <Future<int>>[
231+
for (final key in keys)
232+
sendCommand(['TTL', key]).then(
233+
(v) => v is int ? v : int.tryParse(v.toString()) ?? -1,
234+
onError: (_) => -1,
235+
),
236+
];
237+
final types = await Future.wait(typeFutures);
238+
final ttls = await Future.wait(ttlFutures);
239+
return [
240+
for (var i = 0; i < keys.length; i++)
241+
(type: types[i], ttl: ttls[i]),
242+
];
243+
} finally {
244+
cmd?.pipe_end();
245+
}
246+
}
247+
209248
/// GET (string).
210249
Future<String?> get(String key) async {
211250
final result = await sendCommand(['GET', key]);

lib/features/redis/redis_keys_view.dart

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,24 @@ class _RedisKeysViewState extends material.State<RedisKeysView> {
7878
count: 100,
7979
);
8080

81-
// Fetch type and TTL for each key concurrently
82-
final futures = keyNames.map((name) async {
83-
try {
84-
final type = await widget.connection.keyType(name);
85-
final ttl = await widget.connection.ttl(name);
86-
return _KeyInfo(name: name, type: type, ttl: ttl);
87-
} catch (_) {
88-
return _KeyInfo(name: name, type: 'unknown', ttl: -1);
89-
}
90-
});
91-
final infos = await Future.wait(futures);
81+
// One pipelined burst of TYPE+TTL (not N× Future.wait round-trips).
82+
List<_KeyInfo> infos;
83+
try {
84+
final metas = await widget.connection.typesAndTtls(keyNames);
85+
infos = [
86+
for (var i = 0; i < keyNames.length; i++)
87+
_KeyInfo(
88+
name: keyNames[i],
89+
type: metas[i].type,
90+
ttl: metas[i].ttl,
91+
),
92+
];
93+
} catch (_) {
94+
infos = [
95+
for (final name in keyNames)
96+
_KeyInfo(name: name, type: 'unknown', ttl: -1),
97+
];
98+
}
9299

93100
if (!mounted) return;
94101
setState(() {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:querya_desktop/core/database/redis_connection.dart';
3+
4+
/// Counts outbound commands to prove [typesAndTtls] fires TYPE+TTL without
5+
/// awaiting between keys (true pipeline enqueue).
6+
class _CountingRedisFake extends RedisConnectionTestFake {
7+
_CountingRedisFake() : super(firstScanKeys: const []);
8+
9+
final List<String> ops = [];
10+
11+
@override
12+
Future<dynamic> sendCommand(List<dynamic> args) async {
13+
ops.add(args.first.toString().toUpperCase());
14+
// Delay so overlapping awaits would change order if callers awaited per key.
15+
await Future<void>.delayed(Duration.zero);
16+
return super.sendCommand(args);
17+
}
18+
}
19+
20+
void main() {
21+
test('typesAndTtls enqueues all TYPE then all TTL before settling', () async {
22+
final fake = _CountingRedisFake();
23+
await fake.connect();
24+
25+
final metas = await fake.typesAndTtls(['a', 'b', 'c']);
26+
27+
expect(metas, hasLength(3));
28+
expect(metas.map((m) => m.type), everyElement('string'));
29+
expect(metas.map((m) => m.ttl), everyElement(-1));
30+
31+
// All TYPE writes precede all TTL writes (single burst, not TYPE+TTL per key).
32+
expect(
33+
fake.ops,
34+
['TYPE', 'TYPE', 'TYPE', 'TTL', 'TTL', 'TTL'],
35+
);
36+
});
37+
38+
test('typesAndTtls returns empty for empty keys', () async {
39+
final fake = RedisConnectionTestFake();
40+
await fake.connect();
41+
expect(await fake.typesAndTtls(const []), isEmpty);
42+
});
43+
}

0 commit comments

Comments
 (0)