Skip to content

Commit cb6adbb

Browse files
committed
Add early validation for transaction mode compatibility in the data-loader import command
1 parent ccbd39f commit cb6adbb

File tree

3 files changed

+155
-9
lines changed

3 files changed

+155
-9
lines changed

data-loader/cli/src/main/java/com/scalar/db/dataloader/cli/command/dataimport/ImportCommand.java

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
import static com.scalar.db.dataloader.cli.util.CommandLineInputUtils.validatePositiveValue;
55

66
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.scalar.db.api.DistributedTransaction;
78
import com.scalar.db.api.DistributedTransactionAdmin;
89
import com.scalar.db.api.TableMetadata;
10+
import com.scalar.db.common.CoreError;
911
import com.scalar.db.dataloader.core.DataLoaderError;
1012
import com.scalar.db.dataloader.core.FileFormat;
13+
import com.scalar.db.dataloader.core.ScalarDbMode;
1114
import com.scalar.db.dataloader.core.dataimport.ImportManager;
1215
import com.scalar.db.dataloader.core.dataimport.ImportOptions;
1316
import com.scalar.db.dataloader.core.dataimport.controlfile.ControlFile;
@@ -75,12 +78,24 @@ public Integer call() throws Exception {
7578
.prettyPrint(prettyPrint)
7679
.build();
7780
LogWriterFactory logWriterFactory = createLogWriterFactory(config);
81+
File configFile = new File(configFilePath);
82+
TransactionFactory transactionFactory = TransactionFactory.create(configFile);
83+
84+
// Validate transaction mode configuration before proceeding
85+
validateTransactionMode(transactionFactory);
86+
7887
Map<String, TableMetadata> tableMetadataMap =
79-
createTableMetadataMap(controlFile, namespace, tableName);
88+
createTableMetadataMap(controlFile, namespace, tableName, transactionFactory);
8089
try (BufferedReader reader =
8190
Files.newBufferedReader(Paths.get(sourceFilePath), Charset.defaultCharset())) {
8291
ImportManager importManager =
83-
createImportManager(importOptions, tableMetadataMap, reader, logWriterFactory, config);
92+
createImportManager(
93+
importOptions,
94+
tableMetadataMap,
95+
reader,
96+
logWriterFactory,
97+
config,
98+
transactionFactory);
8499
importManager.startImport();
85100
}
86101
return 0;
@@ -101,14 +116,16 @@ private LogWriterFactory createLogWriterFactory(ImportLoggerConfig config) {
101116
* @param controlFile control file
102117
* @param namespace Namespace
103118
* @param tableName Single table name
119+
* @param transactionFactory transaction factory to use
104120
* @return {@code Map<String, TableMetadata>} a table metadata map
105121
* @throws ParameterException if one of the argument values is wrong
106122
*/
107123
private Map<String, TableMetadata> createTableMetadataMap(
108-
ControlFile controlFile, String namespace, String tableName)
109-
throws IOException, TableMetadataException {
110-
File configFile = new File(configFilePath);
111-
TransactionFactory transactionFactory = TransactionFactory.create(configFile);
124+
ControlFile controlFile,
125+
String namespace,
126+
String tableName,
127+
TransactionFactory transactionFactory)
128+
throws TableMetadataException {
112129
try (DistributedTransactionAdmin transactionAdmin = transactionFactory.getTransactionAdmin()) {
113130
TableMetadataService tableMetadataService = new TableMetadataService(transactionAdmin);
114131
Map<String, TableMetadata> tableMetadataMap = new HashMap<>();
@@ -135,16 +152,17 @@ private Map<String, TableMetadata> createTableMetadataMap(
135152
* @param reader buffered reader with source data
136153
* @param logWriterFactory log writer factory object
137154
* @param config import logging config
155+
* @param transactionFactory transaction factory to use
138156
* @return ImportManager object
139157
*/
140158
private ImportManager createImportManager(
141159
ImportOptions importOptions,
142160
Map<String, TableMetadata> tableMetadataMap,
143161
BufferedReader reader,
144162
LogWriterFactory logWriterFactory,
145-
ImportLoggerConfig config)
163+
ImportLoggerConfig config,
164+
TransactionFactory transactionFactory)
146165
throws IOException {
147-
File configFile = new File(configFilePath);
148166
ImportProcessorFactory importProcessorFactory = new DefaultImportProcessorFactory();
149167
ImportManager importManager =
150168
new ImportManager(
@@ -153,7 +171,7 @@ private ImportManager createImportManager(
153171
importOptions,
154172
importProcessorFactory,
155173
scalarDbMode,
156-
TransactionFactory.create(configFile).getTransactionManager());
174+
transactionFactory.getTransactionManager());
157175
if (importOptions.getLogMode().equals(LogMode.SPLIT_BY_DATA_CHUNK)) {
158176
importManager.addListener(new SplitByDataChunkImportLogger(config, logWriterFactory));
159177
} else {
@@ -236,6 +254,62 @@ private void validateLogDirectory(String logDirectory) throws ParameterException
236254
}
237255
}
238256

257+
/**
258+
* Validate transaction mode configuration by attempting to start and abort a transaction
259+
*
260+
* @param transactionFactory transaction factory to test
261+
* @throws ParameterException if transaction mode is incompatible with the configured transaction
262+
* manager
263+
*/
264+
void validateTransactionMode(TransactionFactory transactionFactory) {
265+
// Only validate when in TRANSACTION mode
266+
if (scalarDbMode != ScalarDbMode.TRANSACTION) {
267+
return;
268+
}
269+
270+
DistributedTransaction transaction = null;
271+
try {
272+
// Try to start a read only transaction to verify the transaction manager is properly
273+
// configured
274+
transaction = transactionFactory.getTransactionManager().startReadOnly();
275+
} catch (Exception e) {
276+
// Check for specific error about beginning transaction not allowed
277+
if (e.getMessage() != null
278+
&& e.getMessage()
279+
.contains(
280+
CoreError.SINGLE_CRUD_OPERATION_TRANSACTION_BEGINNING_TRANSACTION_NOT_ALLOWED
281+
.buildCode())) {
282+
throw new ParameterException(
283+
spec.commandLine(),
284+
DataLoaderError.INVALID_TRANSACTION_MODE.buildMessage(
285+
"The current configuration does not support TRANSACTION mode. "
286+
+ "Please try with STORAGE mode or check your ScalarDB configuration. "
287+
+ "Error: "
288+
+ e.getClass().getSimpleName()
289+
+ " - "
290+
+ e.getMessage()));
291+
}
292+
293+
// Other exceptions - configuration or runtime error
294+
throw new ParameterException(
295+
spec.commandLine(),
296+
DataLoaderError.INVALID_TRANSACTION_MODE.buildMessage(
297+
"Failed to validate transaction mode compatibility. Error: "
298+
+ e.getClass().getSimpleName()
299+
+ " - "
300+
+ e.getMessage()));
301+
} finally {
302+
// Ensure transaction is aborted
303+
if (transaction != null) {
304+
try {
305+
transaction.abort();
306+
} catch (Exception ignored) {
307+
// Ignore errors during cleanup
308+
}
309+
}
310+
}
311+
}
312+
239313
/**
240314
* Generate control file from a valid control file path
241315
*

data-loader/cli/src/test/java/com/scalar/db/dataloader/cli/command/dataimport/ImportCommandTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertThrows;
55
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.when;
68

9+
import com.scalar.db.api.DistributedTransactionManager;
10+
import com.scalar.db.common.CoreError;
711
import com.scalar.db.dataloader.core.FileFormat;
12+
import com.scalar.db.dataloader.core.ScalarDbMode;
813
import com.scalar.db.dataloader.core.dataimport.ImportMode;
14+
import com.scalar.db.exception.transaction.TransactionException;
15+
import com.scalar.db.service.TransactionFactory;
916
import java.io.IOException;
1017
import java.nio.file.Files;
1118
import java.nio.file.Path;
@@ -279,4 +286,63 @@ void call_withoutMaxThreads_shouldDefaultToAvailableProcessors() {
279286
// Verify it was set to available processors
280287
assertEquals(Runtime.getRuntime().availableProcessors(), command.maxThreads);
281288
}
289+
290+
@Test
291+
void validateTransactionMode_withInvalidConfig_shouldThrowException() throws Exception {
292+
// Arrange - Mock TransactionFactory to throw exception with error
293+
TransactionFactory mockFactory = mock(TransactionFactory.class);
294+
DistributedTransactionManager mockManager = mock(DistributedTransactionManager.class);
295+
296+
when(mockFactory.getTransactionManager()).thenReturn(mockManager);
297+
when(mockManager.startReadOnly())
298+
.thenThrow(
299+
new TransactionException(
300+
CoreError.SINGLE_CRUD_OPERATION_TRANSACTION_BEGINNING_TRANSACTION_NOT_ALLOWED
301+
.buildMessage(),
302+
null));
303+
304+
ImportCommand command = new ImportCommand();
305+
CommandLine cmd = new CommandLine(command);
306+
command.spec = cmd.getCommandSpec();
307+
command.scalarDbMode = ScalarDbMode.TRANSACTION;
308+
309+
// Act & Assert
310+
CommandLine.ParameterException thrown =
311+
assertThrows(
312+
CommandLine.ParameterException.class,
313+
() -> command.validateTransactionMode(mockFactory));
314+
315+
assertTrue(thrown.getMessage().contains("does not support TRANSACTION mode"));
316+
assertTrue(
317+
thrown
318+
.getMessage()
319+
.contains(
320+
CoreError.SINGLE_CRUD_OPERATION_TRANSACTION_BEGINNING_TRANSACTION_NOT_ALLOWED
321+
.buildCode()));
322+
}
323+
324+
@Test
325+
void validateTransactionMode_withOtherException_shouldThrowException() throws Exception {
326+
// Arrange - Mock TransactionFactory to throw a different exception
327+
TransactionFactory mockFactory = mock(TransactionFactory.class);
328+
DistributedTransactionManager mockManager = mock(DistributedTransactionManager.class);
329+
330+
when(mockFactory.getTransactionManager()).thenReturn(mockManager);
331+
when(mockManager.startReadOnly()).thenThrow(new RuntimeException("Connection failed"));
332+
333+
ImportCommand command = new ImportCommand();
334+
CommandLine cmd = new CommandLine(command);
335+
command.spec = cmd.getCommandSpec();
336+
command.scalarDbMode = ScalarDbMode.TRANSACTION;
337+
338+
// Act & Assert
339+
CommandLine.ParameterException thrown =
340+
assertThrows(
341+
CommandLine.ParameterException.class,
342+
() -> command.validateTransactionMode(mockFactory));
343+
344+
assertTrue(thrown.getMessage().contains("Failed to validate transaction mode"));
345+
assertTrue(thrown.getMessage().contains("RuntimeException"));
346+
assertTrue(thrown.getMessage().contains("Connection failed"));
347+
}
282348
}

data-loader/core/src/main/java/com/scalar/db/dataloader/core/DataLoaderError.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ public enum DataLoaderError implements ScalarDbError {
216216
"Cannot specify both deprecated option '%s' and new option '%s'. Please use only '%s'",
217217
"",
218218
""),
219+
INVALID_TRANSACTION_MODE(
220+
Category.USER_ERROR,
221+
"0058",
222+
"TRANSACTION mode is not compatible with the current configuration. %s",
223+
"",
224+
""),
219225

220226
//
221227
// Errors for the internal error category

0 commit comments

Comments
 (0)