Skip to content

Commit b6a023e

Browse files
fix: implement code review improvements for Redis transient mode
Addressed all review points from gemini-code-assist and claude: 1. Type Safety: - Replaced Object[] return type with type-safe ResolvedKey record - Eliminates runtime casting and improves code maintainability 2. Atomicity Improvements: - Wrapped hDel transient operations in transaction for atomicity - Wrapped hSet transient operations in transaction for consistency - Simplified getDel to use atomic setGlobalVariable return value - Fixed removeVariable to use atomic setGlobalVariable return 3. Error Handling: - Added try-catch in select() method for non-existent databases - Added FINE level logging when database prefix resolution fails - Better error messages for invalid database names 4. Code Clarity: - Reverted getRecord to throw exception for non-RID keys - Removes implicit null return contract - Makes calling code more predictable and robust All transient mode Redis operations are now fully atomic and thread-safe. Co-authored-by: Luca Garulli <lvca@users.noreply.github.com>
1 parent c18b9ae commit b6a023e

File tree

2 files changed

+72
-66
lines changed

2 files changed

+72
-66
lines changed

redisw/src/main/java/com/arcadedb/redis/RedisNetworkExecutor.java

Lines changed: 70 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public class RedisNetworkExecutor extends Thread {
5959
private final Map<String, Object> defaultBucket = new ConcurrentHashMap<>();
6060
private DatabaseInternal selectedDatabase = null;
6161

62+
/**
63+
* Holds the resolved key and database from key resolution.
64+
*/
65+
private record ResolvedKey(String key, DatabaseInternal database) {
66+
}
67+
6268
public RedisNetworkExecutor(final ArcadeDBServer server, final Socket socket) throws IOException {
6369
setName(Constants.PRODUCT + "-redis/" + socket.getInetAddress());
6470
this.server = server;
@@ -255,18 +261,21 @@ private void hDel(final List<Object> list) {
255261
final String bucketName = (String) list.get(1);
256262

257263
final int pos = bucketName.indexOf(".");
258-
int deleted = 0;
264+
final int[] deleted = {0};
259265

260266
if (pos < 0) {
261-
// Transient mode: delete from globalVariables
267+
// Transient mode: delete from globalVariables atomically
262268
final DatabaseInternal database = (DatabaseInternal) server.getDatabase(bucketName);
263-
for (int i = 2; i < list.size(); i++) {
264-
final String key = (String) list.get(i);
265-
if (database.getGlobalVariable(key) != null) {
266-
database.setGlobalVariable(key, null);
267-
++deleted;
269+
database.transaction(() -> {
270+
for (int i = 2; i < list.size(); i++) {
271+
final String key = (String) list.get(i);
272+
// Use setGlobalVariable which atomically returns the previous value
273+
final Object previous = database.setGlobalVariable(key, null);
274+
if (previous != null) {
275+
deleted[0]++;
276+
}
268277
}
269-
}
278+
});
270279
} else {
271280
// Persistent mode: delete from database
272281
final String databaseName = bucketName.substring(0, pos);
@@ -276,7 +285,7 @@ private void hDel(final List<Object> list) {
276285

277286
if (keyType.startsWith("#")) {
278287
new RID(database, keyType).getRecord().delete();
279-
++deleted;
288+
deleted[0]++;
280289
} else {
281290
final Index index = database.getSchema().getIndexByName(keyType);
282291

@@ -294,13 +303,13 @@ private void hDel(final List<Object> list) {
294303
final IndexCursor cursor = index.get(keys);
295304
if (cursor.hasNext()) {
296305
cursor.next().getRecord().delete();
297-
++deleted;
306+
deleted[0]++;
298307
}
299308
}
300309
}
301310
}
302311
value.append(":");
303-
value.append(deleted);
312+
value.append(deleted[0]);
304313
}
305314

306315
private void hExists(final List<Object> list) {
@@ -383,20 +392,22 @@ private void hSet(final List<Object> list) {
383392

384393
// Check if transient mode: second argument is JSON (starts with '{')
385394
if (secondArg.startsWith("{")) {
386-
// Transient mode: store JSON objects in globalVariables
395+
// Transient mode: store JSON objects in globalVariables atomically
387396
final DatabaseInternal database = (DatabaseInternal) server.getDatabase(databaseName);
388-
int stored = 0;
389-
for (int i = 2; i < list.size(); i++) {
390-
final JSONObject json = new JSONObject((String) list.get(i));
391-
if (!json.has("id")) {
392-
throw new RedisException("JSON object must have an 'id' field for transient storage");
397+
final int[] stored = {0};
398+
database.transaction(() -> {
399+
for (int i = 2; i < list.size(); i++) {
400+
final JSONObject json = new JSONObject((String) list.get(i));
401+
if (!json.has("id")) {
402+
throw new RedisException("JSON object must have an 'id' field for transient storage");
403+
}
404+
final String key = json.get("id").toString();
405+
database.setGlobalVariable(key, json.toString());
406+
stored[0]++;
393407
}
394-
final String key = json.get("id").toString();
395-
database.setGlobalVariable(key, json.toString());
396-
stored++;
397-
}
408+
});
398409
value.append(":");
399-
value.append(stored);
410+
value.append(stored[0]);
400411
} else {
401412
// Persistent mode: store documents in database type
402413
final String typeName = secondArg;
@@ -469,80 +480,78 @@ private void ping(final List<Object> list) {
469480

470481
private void select(final List<Object> list) {
471482
final String dbName = (String) list.get(1);
472-
this.selectedDatabase = (DatabaseInternal) server.getDatabase(dbName);
473-
value.append("+");
474-
value.append("OK");
483+
try {
484+
this.selectedDatabase = (DatabaseInternal) server.getDatabase(dbName);
485+
value.append("+");
486+
value.append("OK");
487+
} catch (final Exception e) {
488+
throw new RedisException("Database '" + dbName + "' not found");
489+
}
475490
}
476491

477492
/**
478493
* Resolves the database and actual key from a potentially prefixed key.
479494
* Priority: key prefix (dbname.key) > SELECT > default config > connection-local bucket.
480495
*
481496
* @param key the key which may contain a database prefix (e.g., "mydb.mykey")
482-
* @return array of [resolvedKey, database] where database may be null if using local bucket
497+
* @return ResolvedKey containing the resolved key and database (database may be null if using local bucket)
483498
*/
484-
private Object[] resolveKeyAndDatabase(final String key) {
499+
private ResolvedKey resolveKeyAndDatabase(final String key) {
485500
// Check for database prefix (dbname.key)
486501
final int dotPos = key.indexOf('.');
487502
if (dotPos > 0) {
488503
final String dbName = key.substring(0, dotPos);
489504
final String actualKey = key.substring(dotPos + 1);
490505
try {
491-
return new Object[]{actualKey, (DatabaseInternal) server.getDatabase(dbName)};
506+
return new ResolvedKey(actualKey, (DatabaseInternal) server.getDatabase(dbName));
492507
} catch (final Exception e) {
508+
LogManager.instance().log(this, Level.FINE,
509+
"Could not resolve database '%s' from key '%s'. Treating as a regular key. Error: %s", e, dbName, key,
510+
e.getMessage());
493511
// Not a valid database prefix, treat as regular key
494512
}
495513
}
496514

497515
// Use selected database (from SELECT command or default config)
498-
return new Object[]{key, selectedDatabase};
516+
return new ResolvedKey(key, selectedDatabase);
499517
}
500518

501519
private Object getVariable(final String key) {
502-
final Object[] resolved = resolveKeyAndDatabase(key);
503-
final String actualKey = (String) resolved[0];
504-
final DatabaseInternal db = (DatabaseInternal) resolved[1];
520+
final ResolvedKey resolved = resolveKeyAndDatabase(key);
505521

506-
if (db != null) {
507-
return db.getGlobalVariable(actualKey);
522+
if (resolved.database() != null) {
523+
return resolved.database().getGlobalVariable(resolved.key());
508524
}
509-
return defaultBucket.get(actualKey);
525+
return defaultBucket.get(resolved.key());
510526
}
511527

512528
private void setVariable(final String key, final Object value) {
513-
final Object[] resolved = resolveKeyAndDatabase(key);
514-
final String actualKey = (String) resolved[0];
515-
final DatabaseInternal db = (DatabaseInternal) resolved[1];
529+
final ResolvedKey resolved = resolveKeyAndDatabase(key);
516530

517-
if (db != null) {
518-
db.setGlobalVariable(actualKey, value);
531+
if (resolved.database() != null) {
532+
resolved.database().setGlobalVariable(resolved.key(), value);
519533
} else {
520-
defaultBucket.put(actualKey, value);
534+
defaultBucket.put(resolved.key(), value);
521535
}
522536
}
523537

524538
private Object removeVariable(final String key) {
525-
final Object[] resolved = resolveKeyAndDatabase(key);
526-
final String actualKey = (String) resolved[0];
527-
final DatabaseInternal db = (DatabaseInternal) resolved[1];
528-
529-
if (db != null) {
530-
final Object oldValue = db.getGlobalVariable(actualKey);
531-
db.setGlobalVariable(actualKey, null);
532-
return oldValue;
539+
final ResolvedKey resolved = resolveKeyAndDatabase(key);
540+
541+
if (resolved.database() != null) {
542+
// Use setGlobalVariable which atomically returns the previous value
543+
return resolved.database().setGlobalVariable(resolved.key(), null);
533544
}
534-
return defaultBucket.remove(actualKey);
545+
return defaultBucket.remove(resolved.key());
535546
}
536547

537548
private boolean containsVariable(final String key) {
538-
final Object[] resolved = resolveKeyAndDatabase(key);
539-
final String actualKey = (String) resolved[0];
540-
final DatabaseInternal db = (DatabaseInternal) resolved[1];
549+
final ResolvedKey resolved = resolveKeyAndDatabase(key);
541550

542-
if (db != null) {
543-
return db.getGlobalVariable(actualKey) != null;
551+
if (resolved.database() != null) {
552+
return resolved.database().getGlobalVariable(resolved.key()) != null;
544553
}
545-
return defaultBucket.containsKey(actualKey);
554+
return defaultBucket.containsKey(resolved.key());
546555
}
547556

548557
private Object parseNext() throws IOException {
@@ -663,14 +672,13 @@ private byte readNext() throws IOException {
663672
}
664673

665674
/**
666-
* Gets a record by RID, index, or from globalVariables (transient).
675+
* Gets a record by RID or index.
667676
* Formats:
668677
* - bucketName = database, key = #rid -> get by RID
669-
* - bucketName = database, key = id (not starting with #) -> get from globalVariables (transient)
670678
* - bucketName = database.indexName, key = value -> get by index
671679
*
672-
* @return the record, or null if not found. For transient mode, returns null but the value
673-
* can be retrieved via getTransientValue() if needed for special handling.
680+
* @return the record, or null if not found
681+
* @throws RedisException if key is not a RID when bucketName has no dot
674682
*/
675683
private Record getRecord(final String bucketName, final String key) {
676684
final Record record;
@@ -682,9 +690,8 @@ private Record getRecord(final String bucketName, final String key) {
682690
// BY RID
683691
record = new RID(database, key).asDocument();
684692
} else {
685-
// TRANSIENT MODE: get from globalVariables
686-
// Return null here - caller should use getTransientValue for the actual value
687-
record = null;
693+
throw new RedisException(
694+
"Retrieving a record by RID, the key must be as #<bucket-id>:<bucket-position>. Example: #13:432");
688695
}
689696
} else {
690697
// BY INDEX

redisw/src/main/java/com/arcadedb/redis/query/RedisQueryEngine.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,8 @@ private Object getDel(final List<String> parts) {
316316
throw new CommandParsingException("GETDEL requires a key: GETDEL <key>");
317317
}
318318
final String key = parts.get(1);
319-
final Object value = database.getGlobalVariable(key);
320-
database.setGlobalVariable(key, null); // Setting to null removes the variable
321-
return value;
319+
// Use setGlobalVariable which atomically returns the previous value
320+
return database.setGlobalVariable(key, null);
322321
}
323322

324323
private int exists(final List<String> parts) {

0 commit comments

Comments
 (0)