Skip to content

Commit 8cf94e2

Browse files
fix: implement code review recommendations for BOLT protocol
Critical fixes: - Close database instance in cleanup() to prevent resource leak - Close ResultSet in multiple handlers (RESET, PULL, DISCARD, ROLLBACK) - Add bucket ID validation in ridToId() to prevent integer overflow - Add thread safety documentation for instance variables Improvements: - Extract authentication logic into reusable helper method - Add configurable routing table TTL (BOLT_ROUTING_TTL) - Centralize error codes in BoltErrorCodes class - Add TODOs for timing metrics and query type determination - Add TODO for database selection behavior clarification Co-authored-by: Luca Garulli <lvca@users.noreply.github.com>
1 parent b48ff6b commit 8cf94e2

File tree

6 files changed

+170
-58
lines changed

6 files changed

+170
-58
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.bolt;
20+
21+
/**
22+
* Centralized constants for Neo4j-compatible BOLT error codes.
23+
* These error codes are used across BOLT protocol messages and exceptions.
24+
*/
25+
public final class BoltErrorCodes {
26+
private BoltErrorCodes() {
27+
// Utility class - prevent instantiation
28+
}
29+
30+
// Security errors
31+
public static final String AUTHENTICATION_ERROR = "Neo.ClientError.Security.Unauthorized";
32+
public static final String FORBIDDEN_ERROR = "Neo.ClientError.Security.Forbidden";
33+
34+
// Statement errors
35+
public static final String SYNTAX_ERROR = "Neo.ClientError.Statement.SyntaxError";
36+
public static final String SEMANTIC_ERROR = "Neo.ClientError.Statement.SemanticError";
37+
38+
// Transaction errors
39+
public static final String TRANSACTION_ERROR = "Neo.ClientError.Transaction.TransactionNotFound";
40+
41+
// Request errors
42+
public static final String PROTOCOL_ERROR = "Neo.ClientError.Request.Invalid";
43+
44+
// Database errors
45+
public static final String DATABASE_ERROR = "Neo.DatabaseError.General.UnknownError";
46+
}

bolt/src/main/java/com/arcadedb/bolt/BoltException.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222

2323
/**
2424
* Exception for BOLT protocol errors.
25+
* Error codes are defined in {@link BoltErrorCodes}.
2526
*/
2627
public class BoltException extends ArcadeDBException {
2728
private final String errorCode;
2829

2930
public BoltException(final String message) {
3031
super(message);
31-
this.errorCode = "Neo.DatabaseError.General.UnknownError";
32+
this.errorCode = BoltErrorCodes.DATABASE_ERROR;
3233
}
3334

3435
public BoltException(final String errorCode, final String message) {
@@ -38,7 +39,7 @@ public BoltException(final String errorCode, final String message) {
3839

3940
public BoltException(final String message, final Throwable cause) {
4041
super(message, cause);
41-
this.errorCode = "Neo.DatabaseError.General.UnknownError";
42+
this.errorCode = BoltErrorCodes.DATABASE_ERROR;
4243
}
4344

4445
public BoltException(final String errorCode, final String message, final Throwable cause) {
@@ -50,12 +51,26 @@ public String getErrorCode() {
5051
return errorCode;
5152
}
5253

53-
// Common Neo4j error codes
54-
public static final String AUTHENTICATION_ERROR = "Neo.ClientError.Security.Unauthorized";
55-
public static final String SYNTAX_ERROR = "Neo.ClientError.Statement.SyntaxError";
56-
public static final String SEMANTIC_ERROR = "Neo.ClientError.Statement.SemanticError";
57-
public static final String DATABASE_ERROR = "Neo.DatabaseError.General.UnknownError";
58-
public static final String TRANSACTION_ERROR = "Neo.ClientError.Transaction.TransactionNotFound";
59-
public static final String FORBIDDEN_ERROR = "Neo.ClientError.Security.Forbidden";
60-
public static final String PROTOCOL_ERROR = "Neo.ClientError.Request.Invalid";
54+
// Deprecated: Use BoltErrorCodes constants instead
55+
/** @deprecated Use {@link BoltErrorCodes#AUTHENTICATION_ERROR} */
56+
@Deprecated
57+
public static final String AUTHENTICATION_ERROR = BoltErrorCodes.AUTHENTICATION_ERROR;
58+
/** @deprecated Use {@link BoltErrorCodes#SYNTAX_ERROR} */
59+
@Deprecated
60+
public static final String SYNTAX_ERROR = BoltErrorCodes.SYNTAX_ERROR;
61+
/** @deprecated Use {@link BoltErrorCodes#SEMANTIC_ERROR} */
62+
@Deprecated
63+
public static final String SEMANTIC_ERROR = BoltErrorCodes.SEMANTIC_ERROR;
64+
/** @deprecated Use {@link BoltErrorCodes#DATABASE_ERROR} */
65+
@Deprecated
66+
public static final String DATABASE_ERROR = BoltErrorCodes.DATABASE_ERROR;
67+
/** @deprecated Use {@link BoltErrorCodes#TRANSACTION_ERROR} */
68+
@Deprecated
69+
public static final String TRANSACTION_ERROR = BoltErrorCodes.TRANSACTION_ERROR;
70+
/** @deprecated Use {@link BoltErrorCodes#FORBIDDEN_ERROR} */
71+
@Deprecated
72+
public static final String FORBIDDEN_ERROR = BoltErrorCodes.FORBIDDEN_ERROR;
73+
/** @deprecated Use {@link BoltErrorCodes#PROTOCOL_ERROR} */
74+
@Deprecated
75+
public static final String PROTOCOL_ERROR = BoltErrorCodes.PROTOCOL_ERROR;
6176
}

bolt/src/main/java/com/arcadedb/bolt/BoltNetworkExecutor.java

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ private enum State {
8080
// Transaction state
8181
private boolean explicitTransaction = false;
8282

83-
// Current result set for streaming
83+
/**
84+
* Current result set for streaming results.
85+
* Thread-safety: This class is designed to handle a single connection in a dedicated thread.
86+
* All state variables are accessed only by the executor thread and do not require synchronization.
87+
*/
8488
private ResultSet currentResultSet;
8589
private List<String> currentFields;
8690
private Result firstResult; // Buffered first result for field name extraction
@@ -275,16 +279,7 @@ private void handleHello(final HelloMessage message) throws IOException {
275279

276280
// Try to authenticate
277281
if ("basic".equals(scheme) && principal != null && credentials != null) {
278-
try {
279-
user = server.getSecurity().authenticate(principal, credentials, databaseName);
280-
if (user == null) {
281-
sendFailure(BoltException.AUTHENTICATION_ERROR, "Invalid credentials");
282-
state = State.FAILED;
283-
return;
284-
}
285-
} catch (final ServerSecurityException e) {
286-
sendFailure(BoltException.AUTHENTICATION_ERROR, e.getMessage());
287-
state = State.FAILED;
282+
if (!authenticateUser(principal, credentials)) {
288283
return;
289284
}
290285
} else if ("none".equals(scheme)) {
@@ -294,16 +289,7 @@ private void handleHello(final HelloMessage message) throws IOException {
294289
return;
295290
} else if (principal != null && credentials != null) {
296291
// Try basic auth even without explicit scheme
297-
try {
298-
user = server.getSecurity().authenticate(principal, credentials, databaseName);
299-
if (user == null) {
300-
sendFailure(BoltException.AUTHENTICATION_ERROR, "Invalid credentials");
301-
state = State.FAILED;
302-
return;
303-
}
304-
} catch (final ServerSecurityException e) {
305-
sendFailure(BoltException.AUTHENTICATION_ERROR, e.getMessage());
306-
state = State.FAILED;
292+
if (!authenticateUser(principal, credentials)) {
307293
return;
308294
}
309295
}
@@ -331,19 +317,8 @@ private void handleLogon(final LogonMessage message) throws IOException {
331317
final String principal = message.getPrincipal();
332318
final String credentials = message.getCredentials();
333319

334-
if (principal != null && credentials != null) {
335-
try {
336-
user = server.getSecurity().authenticate(principal, credentials, databaseName);
337-
if (user == null) {
338-
sendFailure(BoltException.AUTHENTICATION_ERROR, "Invalid credentials");
339-
state = State.FAILED;
340-
return;
341-
}
342-
} catch (final ServerSecurityException e) {
343-
sendFailure(BoltException.AUTHENTICATION_ERROR, e.getMessage());
344-
state = State.FAILED;
345-
return;
346-
}
320+
if (!authenticateUser(principal, credentials)) {
321+
return;
347322
}
348323

349324
sendSuccess(Map.of());
@@ -370,6 +345,15 @@ private void handleGoodbye() {
370345
* Handle RESET message - reset to initial state.
371346
*/
372347
private void handleReset() throws IOException {
348+
// Close any open result set
349+
if (currentResultSet != null) {
350+
try {
351+
currentResultSet.close();
352+
} catch (final Exception e) {
353+
// Ignore
354+
}
355+
}
356+
373357
// Rollback any open transaction
374358
if (explicitTransaction && database != null) {
375359
try {
@@ -430,7 +414,8 @@ private void handleRun(final RunMessage message) throws IOException {
430414
// Build success response with query metadata
431415
final Map<String, Object> metadata = new LinkedHashMap<>();
432416
metadata.put("fields", currentFields);
433-
metadata.put("t_first", 0L); // Time to first record (placeholder)
417+
// TODO: Implement actual time to first record calculation for accurate performance monitoring
418+
metadata.put("t_first", 0L);
434419

435420
sendSuccess(metadata);
436421
state = explicitTransaction ? State.TX_STREAMING : State.STREAMING;
@@ -503,8 +488,15 @@ private void handlePull(final PullMessage message) throws IOException {
503488
// Build success metadata
504489
final Map<String, Object> metadata = new LinkedHashMap<>();
505490
if (!hasMore) {
506-
metadata.put("type", "r"); // Read-only query type
507-
metadata.put("t_last", 0L); // Time to last record
491+
// TODO: Determine query type dynamically (r=read, w=write, rw=read-write, s=schema)
492+
metadata.put("type", "r");
493+
// TODO: Implement actual time to last record calculation for accurate performance metrics
494+
metadata.put("t_last", 0L);
495+
try {
496+
currentResultSet.close();
497+
} catch (final Exception e) {
498+
// Ignore
499+
}
508500
currentResultSet = null;
509501
currentFields = null;
510502
firstResult = null;
@@ -541,6 +533,11 @@ private void handleDiscard(final DiscardMessage message) throws IOException {
541533
while (currentResultSet.hasNext()) {
542534
currentResultSet.next();
543535
}
536+
try {
537+
currentResultSet.close();
538+
} catch (final Exception e) {
539+
// Ignore
540+
}
544541
}
545542

546543
currentResultSet = null;
@@ -641,6 +638,13 @@ private void handleRollback() throws IOException {
641638
}
642639

643640
try {
641+
if (currentResultSet != null) {
642+
try {
643+
currentResultSet.close();
644+
} catch (final Exception e) {
645+
// Ignore
646+
}
647+
}
644648
if (database != null) {
645649
database.rollback();
646650
}
@@ -673,7 +677,7 @@ private void handleRoute(final RouteMessage message) throws IOException {
673677
final String address = host + ":" + port;
674678

675679
final Map<String, Object> rt = new LinkedHashMap<>();
676-
rt.put("ttl", 300L); // 5 minute TTL
680+
rt.put("ttl", GlobalConfiguration.BOLT_ROUTING_TTL.getValueAsLong());
677681
rt.put("db", message.getDatabase() != null ? message.getDatabase() : databaseName);
678682

679683
final List<Map<String, Object>> servers = new ArrayList<>();
@@ -710,7 +714,8 @@ private boolean ensureDatabase() throws IOException {
710714
}
711715

712716
if (databaseName == null || databaseName.isEmpty()) {
713-
// Try to get default database or first available
717+
// TODO: Consider making default database selection configurable or requiring explicit database name
718+
// to avoid unpredictable behavior in multi-database environments
714719
final Collection<String> databases = server.getDatabaseNames();
715720
if (databases.isEmpty()) {
716721
sendFailure(BoltException.DATABASE_ERROR, "No database available");
@@ -761,6 +766,34 @@ private String generateBookmark() {
761766
return "arcade:tx:" + System.currentTimeMillis();
762767
}
763768

769+
/**
770+
* Authenticate user with provided credentials.
771+
*
772+
* @param principal the username
773+
* @param credentials the password
774+
* @return true if authentication succeeded, false otherwise (failure already sent)
775+
* @throws IOException if sending failure message fails
776+
*/
777+
private boolean authenticateUser(final String principal, final String credentials) throws IOException {
778+
if (principal == null || credentials == null) {
779+
return false;
780+
}
781+
782+
try {
783+
user = server.getSecurity().authenticate(principal, credentials, databaseName);
784+
if (user == null) {
785+
sendFailure(BoltException.AUTHENTICATION_ERROR, "Invalid credentials");
786+
state = State.FAILED;
787+
return false;
788+
}
789+
return true;
790+
} catch (final ServerSecurityException e) {
791+
sendFailure(BoltException.AUTHENTICATION_ERROR, e.getMessage());
792+
state = State.FAILED;
793+
return false;
794+
}
795+
}
796+
764797
/**
765798
* Send a SUCCESS response message.
766799
*/
@@ -809,6 +842,15 @@ private void sendMessage(final BoltMessage message) throws IOException {
809842
* Cleanup resources when connection closes.
810843
*/
811844
private void cleanup() {
845+
try {
846+
if (currentResultSet != null) {
847+
currentResultSet.close();
848+
currentResultSet = null;
849+
}
850+
} catch (final Exception e) {
851+
// Ignore
852+
}
853+
812854
try {
813855
if (explicitTransaction && database != null) {
814856
database.rollback();
@@ -817,6 +859,15 @@ private void cleanup() {
817859
// Ignore
818860
}
819861

862+
try {
863+
if (database != null) {
864+
database.close();
865+
database = null;
866+
}
867+
} catch (final Exception e) {
868+
// Ignore
869+
}
870+
820871
try {
821872
socket.close();
822873
} catch (final Exception e) {

bolt/src/main/java/com/arcadedb/bolt/message/FailureMessage.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,4 @@ public void writeTo(final PackStreamWriter writer) throws IOException {
6666
public String toString() {
6767
return "FAILURE{code=" + getCode() + ", message=" + getMessage() + "}";
6868
}
69-
70-
// Common Neo4j error codes
71-
public static final String AUTHENTICATION_ERROR = "Neo.ClientError.Security.Unauthorized";
72-
public static final String SYNTAX_ERROR = "Neo.ClientError.Statement.SyntaxError";
73-
public static final String SEMANTIC_ERROR = "Neo.ClientError.Statement.SemanticError";
74-
public static final String DATABASE_ERROR = "Neo.DatabaseError.General.UnknownError";
75-
public static final String TRANSACTION_ERROR = "Neo.ClientError.Transaction.TransactionNotFound";
76-
public static final String FORBIDDEN_ERROR = "Neo.ClientError.Security.Forbidden";
7769
}

bolt/src/main/java/com/arcadedb/bolt/structure/BoltStructureMapper.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,12 @@ public static long ridToId(final RID rid) {
307307
if (rid == null) {
308308
return -1;
309309
}
310+
final int bucketId = rid.getBucketId();
311+
// Validate bucket ID to prevent overflow (max 16 bits)
312+
if (bucketId < 0 || bucketId > 0xFFFF) {
313+
throw new IllegalArgumentException("Bucket ID out of range for BOLT ID conversion: " + bucketId);
314+
}
310315
// Combine bucket ID (high bits) and position (low bits)
311-
return ((long) rid.getBucketId() << 48) | (rid.getPosition() & 0xFFFFFFFFFFFFL);
316+
return ((long) bucketId << 48) | (rid.getPosition() & 0xFFFFFFFFFFFFL);
312317
}
313318
}

engine/src/main/java/com/arcadedb/GlobalConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,9 @@ This setting is intended as a safety measure against excessive resource consumpt
518518
BOLT_DEBUG("arcadedb.bolt.debug", SCOPE.SERVER,
519519
"Enables the printing of BOLT protocol to the console. Default is false", Boolean.class, false),
520520

521+
BOLT_ROUTING_TTL("arcadedb.bolt.routing.ttl", SCOPE.SERVER,
522+
"Time-to-live (in seconds) for BOLT routing table entries. Default is 300 (5 minutes)", Long.class, 300L),
523+
521524
// REDIS
522525
REDIS_PORT("arcadedb.redis.port", SCOPE.SERVER,
523526
"TCP/IP port number used for incoming connections for Redis plugin. Default is 6379", Integer.class, 6379),

0 commit comments

Comments
 (0)