From 0f55d39bb8492a14882fc875d54f66c41ebad1f1 Mon Sep 17 00:00:00 2001 From: Jun Nemoto Date: Mon, 10 Nov 2025 14:09:28 +0900 Subject: [PATCH 1/5] Add namespace-creation feature --- .../scalar/dl/client/error/ClientError.java | 2 + .../dl/client/service/AuditorClient.java | 12 + .../com/scalar/dl/client/service/Client.java | 3 + .../dl/client/service/ClientService.java | 36 +++ .../client/service/ClientServiceHandler.java | 3 + .../service/DefaultClientServiceHandler.java | 20 ++ .../dl/client/service/GatewayClient.java | 12 + .../service/GatewayClientServiceHandler.java | 6 + .../dl/client/service/LedgerClient.java | 12 + .../dl/client/service/ClientServiceTest.java | 91 ++++++++ .../scalar/dl/ledger/config/ServerConfig.java | 2 + .../scalar/dl/ledger/error/CommonError.java | 16 ++ .../dl/ledger/exception/LedgerException.java | 4 + .../scalar/dl/ledger/service/StatusCode.java | 3 + .../service/LedgerServiceIntegrationTest.java | 34 ++- .../scalar/dl/ledger/config/LedgerConfig.java | 1 + .../dl/ledger/database/NamespaceRegistry.java | 6 + .../AbstractScalarNamespaceRegistry.java | 161 ++++++++++++++ .../scalardb/LedgerNamespaceRegistry.java | 21 ++ .../scalardb/ScalarCertificateRegistry.java | 20 +- .../scalardb/ScalarContractRegistry.java | 34 ++- .../scalardb/ScalarFunctionRegistry.java | 19 +- .../scalardb/ScalarSecretRegistry.java | 20 +- .../ScalarTamperEvidentAssetLedger.java | 27 +++ .../scalardb/ScalarTransactionManager.java | 8 +- .../scalardb/TableMetadataProvider.java | 16 ++ .../model/NamespaceCreationRequest.java | 52 +++++ .../dl/ledger/namespace/NamespaceManager.java | 49 ++++ .../server/LedgerPrivilegedService.java | 8 + .../dl/ledger/server/TypeConverter.java | 6 + .../scalar/dl/ledger/service/BaseService.java | 11 +- .../dl/ledger/service/LedgerModule.java | 55 ++++- .../dl/ledger/service/LedgerService.java | 5 + .../service/LedgerServicePermissionTest.java | 5 +- .../dl/ledger/config/LedgerConfigTest.java | 1 - .../scalardb/LedgerNamespaceRegistryTest.java | 209 ++++++++++++++++++ .../model/NamespaceCreationRequestTest.java | 119 ++++++++++ .../namespace/NamespaceManagerTest.java | 126 +++++++++++ .../dl/ledger/service/BaseServiceTest.java | 22 ++ rpc/src/main/proto/scalar.proto | 10 + 40 files changed, 1249 insertions(+), 18 deletions(-) create mode 100644 ledger/src/main/java/com/scalar/dl/ledger/database/NamespaceRegistry.java create mode 100644 ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java create mode 100644 ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistry.java create mode 100644 ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/TableMetadataProvider.java create mode 100644 ledger/src/main/java/com/scalar/dl/ledger/model/NamespaceCreationRequest.java create mode 100644 ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java create mode 100644 ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java create mode 100644 ledger/src/test/java/com/scalar/dl/ledger/model/NamespaceCreationRequestTest.java create mode 100644 ledger/src/test/java/com/scalar/dl/ledger/namespace/NamespaceManagerTest.java diff --git a/client/src/main/java/com/scalar/dl/client/error/ClientError.java b/client/src/main/java/com/scalar/dl/client/error/ClientError.java index 55588e6a..5fcc3cbd 100644 --- a/client/src/main/java/com/scalar/dl/client/error/ClientError.java +++ b/client/src/main/java/com/scalar/dl/client/error/ClientError.java @@ -102,6 +102,8 @@ public enum ClientError implements ScalarDlError { "The specified keys are incorrect for the asset type.", "", ""), + SERVICE_NAMESPACE_NAME_CANNOT_BE_NULL( + StatusCode.INVALID_ARGUMENT, "025", "The namespace name cannot be null.", "", ""), // // Errors for RUNTIME_ERROR(502) diff --git a/client/src/main/java/com/scalar/dl/client/service/AuditorClient.java b/client/src/main/java/com/scalar/dl/client/service/AuditorClient.java index e609938d..554d56a3 100644 --- a/client/src/main/java/com/scalar/dl/client/service/AuditorClient.java +++ b/client/src/main/java/com/scalar/dl/client/service/AuditorClient.java @@ -16,6 +16,7 @@ import com.scalar.dl.rpc.ContractsListingRequest; import com.scalar.dl.rpc.ExecutionOrderingResponse; import com.scalar.dl.rpc.ExecutionValidationRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import io.grpc.ManagedChannel; import io.grpc.netty.NettyChannelBuilder; @@ -137,6 +138,17 @@ public ContractExecutionResponse validate(ExecutionValidationRequest request) { return ContractExecutionResponse.getDefaultInstance(); } + @Override + public void create(NamespaceCreationRequest request) { + ThrowableConsumer f = + r -> getAuditorPrivilegedStub().createNamespace(r); + try { + accept(f, request); + } catch (Exception e) { + throwExceptionWithStatusCode(e); + } + } + private AuditorGrpc.AuditorBlockingStub getAuditorStub() { return auditorStub.withDeadlineAfter(deadlineDurationMillis, TimeUnit.MILLISECONDS); } diff --git a/client/src/main/java/com/scalar/dl/client/service/Client.java b/client/src/main/java/com/scalar/dl/client/service/Client.java index d4368698..ea724427 100644 --- a/client/src/main/java/com/scalar/dl/client/service/Client.java +++ b/client/src/main/java/com/scalar/dl/client/service/Client.java @@ -9,6 +9,7 @@ import com.scalar.dl.rpc.CertificateRegistrationRequest; import com.scalar.dl.rpc.ContractRegistrationRequest; import com.scalar.dl.rpc.ContractsListingRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import com.scalar.dl.rpc.Status; import io.grpc.Metadata; @@ -29,6 +30,8 @@ public interface Client { JsonObject list(ContractsListingRequest request); + void create(NamespaceCreationRequest request); + default void accept(ThrowableConsumer f, T request) { try { f.accept(request); diff --git a/client/src/main/java/com/scalar/dl/client/service/ClientService.java b/client/src/main/java/com/scalar/dl/client/service/ClientService.java index 92021069..42e669a8 100644 --- a/client/src/main/java/com/scalar/dl/client/service/ClientService.java +++ b/client/src/main/java/com/scalar/dl/client/service/ClientService.java @@ -27,6 +27,7 @@ import com.scalar.dl.rpc.ContractsListingRequest; import com.scalar.dl.rpc.FunctionRegistrationRequest; import com.scalar.dl.rpc.LedgerValidationRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import java.util.Collections; import java.util.List; @@ -985,6 +986,41 @@ public LedgerValidationResult validateLedger(byte[] serializedBinary) { return handler.validateLedger(request); } + /** + * Creates the specified namespace. + * + * @param namespace a namespace name to create + * @throws ClientException if a request fails for some reason + */ + public void createNamespace(String namespace) { + checkClientMode(ClientMode.CLIENT); + checkArgument( + namespace != null, ClientError.SERVICE_NAMESPACE_NAME_CANNOT_BE_NULL.buildMessage()); + NamespaceCreationRequest request = + NamespaceCreationRequest.newBuilder().setNamespace(namespace).build(); + + handler.createNamespace(request); + } + + /** + * Creates the namespace specified with the serialized byte array of a {@code + * NamespaceCreationRequest}. + * + * @param serializedBinary a serialized byte array of {@code NamespaceCreationRequest} + * @throws ClientException if a request fails for some reason + */ + public void createNamespace(byte[] serializedBinary) { + checkClientMode(ClientMode.INTERMEDIARY); + NamespaceCreationRequest request; + try { + request = NamespaceCreationRequest.parseFrom(serializedBinary); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + + handler.createNamespace(request); + } + /** * It does nothing now. It's left here for backward compatibility. * diff --git a/client/src/main/java/com/scalar/dl/client/service/ClientServiceHandler.java b/client/src/main/java/com/scalar/dl/client/service/ClientServiceHandler.java index 0c13ea2d..8556aaf0 100644 --- a/client/src/main/java/com/scalar/dl/client/service/ClientServiceHandler.java +++ b/client/src/main/java/com/scalar/dl/client/service/ClientServiceHandler.java @@ -9,6 +9,7 @@ import com.scalar.dl.rpc.ContractsListingRequest; import com.scalar.dl.rpc.FunctionRegistrationRequest; import com.scalar.dl.rpc.LedgerValidationRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import javax.json.JsonObject; @@ -74,4 +75,6 @@ public interface ClientServiceHandler { * @throws ClientException if a request fails for some reason */ LedgerValidationResult validateLedger(LedgerValidationRequest request); + + void createNamespace(NamespaceCreationRequest request); } diff --git a/client/src/main/java/com/scalar/dl/client/service/DefaultClientServiceHandler.java b/client/src/main/java/com/scalar/dl/client/service/DefaultClientServiceHandler.java index e9b22cfb..b49b9e53 100644 --- a/client/src/main/java/com/scalar/dl/client/service/DefaultClientServiceHandler.java +++ b/client/src/main/java/com/scalar/dl/client/service/DefaultClientServiceHandler.java @@ -18,6 +18,7 @@ import com.scalar.dl.rpc.ExecutionValidationRequest; import com.scalar.dl.rpc.FunctionRegistrationRequest; import com.scalar.dl.rpc.LedgerValidationRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import java.util.HashMap; import java.util.Map; @@ -128,6 +129,12 @@ public LedgerValidationResult validateLedger(LedgerValidationRequest request) { return client.validate(request); } + @Override + public void createNamespace(NamespaceCreationRequest request) { + createAtAuditor(request); + client.create(request); + } + private void registerToAuditor(CertificateRegistrationRequest request) { if (auditorClient == null) { return; @@ -167,6 +174,19 @@ private void registerToAuditor(ContractRegistrationRequest request) { } } + private void createAtAuditor(NamespaceCreationRequest request) { + if (auditorClient == null) { + return; + } + try { + auditorClient.create(request); + } catch (ClientException e) { + if (!e.getStatusCode().equals(StatusCode.NAMESPACE_ALREADY_EXISTS)) { + throw e; + } + } + } + private ContractExecutionRequest order(ContractExecutionRequest request) { if (auditorClient == null) { return request; diff --git a/client/src/main/java/com/scalar/dl/client/service/GatewayClient.java b/client/src/main/java/com/scalar/dl/client/service/GatewayClient.java index ddf16d3c..9c4dba26 100644 --- a/client/src/main/java/com/scalar/dl/client/service/GatewayClient.java +++ b/client/src/main/java/com/scalar/dl/client/service/GatewayClient.java @@ -22,6 +22,7 @@ import com.scalar.dl.rpc.GatewayPrivilegedGrpc; import com.scalar.dl.rpc.LedgerValidationRequest; import com.scalar.dl.rpc.LedgerValidationResponse; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import io.grpc.ManagedChannel; import io.grpc.netty.NettyChannelBuilder; @@ -183,6 +184,17 @@ public LedgerValidationResult validate(LedgerValidationRequest request) { return new LedgerValidationResult(StatusCode.RUNTIME_ERROR, null, null); } + @Override + public void create(NamespaceCreationRequest request) { + ThrowableConsumer f = + r -> getGatewayPrivilegedStub().createNamespace(r); + try { + accept(f, request); + } catch (Exception e) { + throwExceptionWithStatusCode(e); + } + } + private GatewayGrpc.GatewayBlockingStub getGatewayStub() { return gatewayStub.withDeadlineAfter(deadlineDurationMillis, TimeUnit.MILLISECONDS); } diff --git a/client/src/main/java/com/scalar/dl/client/service/GatewayClientServiceHandler.java b/client/src/main/java/com/scalar/dl/client/service/GatewayClientServiceHandler.java index 2e228e8e..9c09919e 100644 --- a/client/src/main/java/com/scalar/dl/client/service/GatewayClientServiceHandler.java +++ b/client/src/main/java/com/scalar/dl/client/service/GatewayClientServiceHandler.java @@ -10,6 +10,7 @@ import com.scalar.dl.rpc.ContractsListingRequest; import com.scalar.dl.rpc.FunctionRegistrationRequest; import com.scalar.dl.rpc.LedgerValidationRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import javax.json.JsonObject; @@ -108,6 +109,11 @@ public LedgerValidationResult validateLedger(LedgerValidationRequest request) { return client.validate(request); } + @Override + public void createNamespace(NamespaceCreationRequest request) { + client.create(request); + } + @VisibleForTesting AbstractGatewayClient getGatewayClient() { return client; diff --git a/client/src/main/java/com/scalar/dl/client/service/LedgerClient.java b/client/src/main/java/com/scalar/dl/client/service/LedgerClient.java index fef24ab1..22df34e9 100644 --- a/client/src/main/java/com/scalar/dl/client/service/LedgerClient.java +++ b/client/src/main/java/com/scalar/dl/client/service/LedgerClient.java @@ -28,6 +28,7 @@ import com.scalar.dl.rpc.LedgerPrivilegedGrpc; import com.scalar.dl.rpc.LedgerValidationRequest; import com.scalar.dl.rpc.LedgerValidationResponse; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import io.grpc.ManagedChannel; import io.grpc.netty.NettyChannelBuilder; @@ -230,6 +231,17 @@ public TransactionState abort(ExecutionAbortRequest request) { return TransactionState.UNKNOWN; } + @Override + public void create(NamespaceCreationRequest request) { + ThrowableConsumer f = + r -> getLedgerPrivilegedStub().createNamespace(r); + try { + accept(f, request); + } catch (Exception e) { + throwExceptionWithStatusCode(e); + } + } + private LedgerGrpc.LedgerBlockingStub getLedgerStub() { return ledgerStub.withDeadlineAfter(deadlineDurationMillis, TimeUnit.MILLISECONDS); } diff --git a/client/src/test/java/com/scalar/dl/client/service/ClientServiceTest.java b/client/src/test/java/com/scalar/dl/client/service/ClientServiceTest.java index a604454a..ad2684f0 100644 --- a/client/src/test/java/com/scalar/dl/client/service/ClientServiceTest.java +++ b/client/src/test/java/com/scalar/dl/client/service/ClientServiceTest.java @@ -724,4 +724,95 @@ public void validateLedger_SerializedBinaryGiven_ShouldValidateProperly() { verify(config, never()).getHmacIdentityConfig(); verify(signer, never()).sign(any(LedgerValidationRequest.Builder.class)); } + + @Test + public void createNamespace_CorrectInputsGiven_ShouldCreateNamespaceProperly() { + // Arrange + when(config.getClientMode()).thenReturn(ClientMode.CLIENT); + String namespace = "test_namespace"; + + // Act + service.createNamespace(namespace); + + // Assert + com.scalar.dl.rpc.NamespaceCreationRequest expected = + com.scalar.dl.rpc.NamespaceCreationRequest.newBuilder().setNamespace(namespace).build(); + verify(handler).createNamespace(expected); + } + + @Test + public void createNamespace_NullNamespaceGiven_ShouldThrowIllegalArgumentException() { + // Arrange + when(config.getClientMode()).thenReturn(ClientMode.CLIENT); + + // Act + Throwable thrown = catchThrowable(() -> service.createNamespace((String) null)); + + // Assert + assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class); + verify(handler, never()).createNamespace(any(com.scalar.dl.rpc.NamespaceCreationRequest.class)); + } + + @Test + public void + createNamespace_NamespaceNameWithIntermediaryModeGiven_ShouldThrowIllegalArgumentException() { + // Arrange + when(config.getClientMode()).thenReturn(ClientMode.INTERMEDIARY); + String namespace = "test_namespace"; + + // Act + Throwable thrown = catchThrowable(() -> service.createNamespace(namespace)); + + // Assert + assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class); + verify(handler, never()).createNamespace(any(com.scalar.dl.rpc.NamespaceCreationRequest.class)); + } + + @Test + public void createNamespace_SerializedBinaryGiven_ShouldCreateNamespaceProperly() { + // Arrange + when(config.getClientMode()).thenReturn(ClientMode.INTERMEDIARY); + com.scalar.dl.rpc.NamespaceCreationRequest expected = + com.scalar.dl.rpc.NamespaceCreationRequest.newBuilder() + .setNamespace("test_namespace") + .build(); + + // Act + service.createNamespace(expected.toByteArray()); + + // Assert + verify(handler).createNamespace(expected); + } + + @Test + public void createNamespace_InvalidSerializedBinaryGiven_ShouldThrowIllegalArgumentException() { + // Arrange + when(config.getClientMode()).thenReturn(ClientMode.INTERMEDIARY); + byte[] invalidBinary = "invalid".getBytes(StandardCharsets.UTF_8); + + // Act + Throwable thrown = catchThrowable(() -> service.createNamespace(invalidBinary)); + + // Assert + assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class); + verify(handler, never()).createNamespace(any(com.scalar.dl.rpc.NamespaceCreationRequest.class)); + } + + @Test + public void + createNamespace_SerializedBinaryClientModeGiven_ShouldThrowIllegalArgumentException() { + // Arrange + when(config.getClientMode()).thenReturn(ClientMode.CLIENT); + com.scalar.dl.rpc.NamespaceCreationRequest request = + com.scalar.dl.rpc.NamespaceCreationRequest.newBuilder() + .setNamespace("test_namespace") + .build(); + + // Act + Throwable thrown = catchThrowable(() -> service.createNamespace(request.toByteArray())); + + // Assert + assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class); + verify(handler, never()).createNamespace(any(com.scalar.dl.rpc.NamespaceCreationRequest.class)); + } } diff --git a/common/src/main/java/com/scalar/dl/ledger/config/ServerConfig.java b/common/src/main/java/com/scalar/dl/ledger/config/ServerConfig.java index b4e384d3..9462603f 100644 --- a/common/src/main/java/com/scalar/dl/ledger/config/ServerConfig.java +++ b/common/src/main/java/com/scalar/dl/ledger/config/ServerConfig.java @@ -6,6 +6,8 @@ public interface ServerConfig { String getName(); + String getNamespace(); + int getPort(); int getPrivilegedPort(); diff --git a/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java b/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java index 43618cd4..c15326da 100644 --- a/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java +++ b/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java @@ -213,6 +213,8 @@ public enum CommonError implements ScalarDlError { "The deserialization type is not supported. Type: %s", "", ""), + INVALID_NAMESPACE_NAME( + StatusCode.INVALID_ARGUMENT, "019", "The namespace name is invalid. Name: %s", "", ""), // // Errors for SECRET_NOT_FOUND(415) @@ -220,6 +222,16 @@ public enum CommonError implements ScalarDlError { SECRET_NOT_FOUND( StatusCode.SECRET_NOT_FOUND, "001", "The specified secret is not found.", "", ""), + // + // Errors for SECRET_ALREADY_REGISTERED(416) + // + NAMESPACE_ALREADY_EXISTS( + StatusCode.NAMESPACE_ALREADY_EXISTS, + "001", + "The specified namespace already exists.", + "", + ""), + // // Errors for DATABASE_ERROR(500) // @@ -241,6 +253,10 @@ public enum CommonError implements ScalarDlError { StatusCode.DATABASE_ERROR, "008", "Getting the contract failed. Details: %s", "", ""), SCANNING_CONTRACT_FAILED( StatusCode.DATABASE_ERROR, "009", "Scanning the contracts failed. Details: %s", "", ""), + CREATING_NAMESPACE_TABLE_FAILED( + StatusCode.DATABASE_ERROR, "010", "Creating the namespace table failed. Details: %s", "", ""), + CREATING_NAMESPACE_FAILED( + StatusCode.DATABASE_ERROR, "011", "Creating the namespace failed. Details: %s", "", ""), // // Errors for RUNTIME_ERROR(502) diff --git a/common/src/main/java/com/scalar/dl/ledger/exception/LedgerException.java b/common/src/main/java/com/scalar/dl/ledger/exception/LedgerException.java index eb9bc0a3..4a903a9e 100644 --- a/common/src/main/java/com/scalar/dl/ledger/exception/LedgerException.java +++ b/common/src/main/java/com/scalar/dl/ledger/exception/LedgerException.java @@ -20,6 +20,10 @@ public LedgerException(ScalarDlError error, Object... args) { this(error.buildMessage(args), error.getStatusCode()); } + public LedgerException(ScalarDlError error, Throwable cause, Object... args) { + this(error.buildMessage(args), cause, error.getStatusCode()); + } + public StatusCode getCode() { return code; } diff --git a/common/src/main/java/com/scalar/dl/ledger/service/StatusCode.java b/common/src/main/java/com/scalar/dl/ledger/service/StatusCode.java index 2c7090ad..f7567e7e 100644 --- a/common/src/main/java/com/scalar/dl/ledger/service/StatusCode.java +++ b/common/src/main/java/com/scalar/dl/ledger/service/StatusCode.java @@ -129,6 +129,9 @@ public enum StatusCode { /** StatusCode: 415. This indicates that the given secret is not found. */ SECRET_NOT_FOUND(415), + /** StatusCode: 416. This indicates that the given namespace already exists. */ + NAMESPACE_ALREADY_EXISTS(416), + /** * StatusCode: 500. This indicates that the system encountered a database error such as IO error. */ diff --git a/ledger/src/integration-test/java/com/scalar/dl/ledger/service/LedgerServiceIntegrationTest.java b/ledger/src/integration-test/java/com/scalar/dl/ledger/service/LedgerServiceIntegrationTest.java index 2c228a77..551c20c4 100644 --- a/ledger/src/integration-test/java/com/scalar/dl/ledger/service/LedgerServiceIntegrationTest.java +++ b/ledger/src/integration-test/java/com/scalar/dl/ledger/service/LedgerServiceIntegrationTest.java @@ -82,6 +82,7 @@ import com.scalar.dl.ledger.database.TransactionManager; import com.scalar.dl.ledger.exception.ContractContextException; import com.scalar.dl.ledger.exception.DatabaseException; +import com.scalar.dl.ledger.exception.LedgerException; import com.scalar.dl.ledger.exception.MissingContractException; import com.scalar.dl.ledger.exception.SignatureException; import com.scalar.dl.ledger.exception.UnloadableContractException; @@ -91,6 +92,8 @@ import com.scalar.dl.ledger.model.ContractExecutionRequest; import com.scalar.dl.ledger.model.ContractExecutionResult; import com.scalar.dl.ledger.model.ContractRegistrationRequest; +import com.scalar.dl.ledger.model.NamespaceCreationRequest; +import com.scalar.dl.ledger.namespace.NamespaceManager; import com.scalar.dl.ledger.service.contract.ContractUsingContext; import com.scalar.dl.ledger.service.contract.Create; import com.scalar.dl.ledger.service.contract.CreateWithJackson; @@ -142,6 +145,7 @@ public class LedgerServiceIntegrationTest { @Mock private ContractManager contractManager; @Mock private ClientIdentityKey clientIdentityKey; @Mock private FunctionManager functionManager; + @Mock private NamespaceManager namespaceManager; private LedgerService service; private DigitalSignatureSigner dsSigner; private DigitalSignatureValidator dsValidator; @@ -165,7 +169,8 @@ private void prepare(boolean isFunctionEnabled) { new ContractExecutor(config, contractManager, functionManager, transactionManager); service = new LedgerService( - new BaseService(certManager, secretManager, clientKeyValidator, contractManager), + new BaseService( + certManager, secretManager, clientKeyValidator, contractManager, namespaceManager), config, clientKeyValidator, auditorKeyValidator, @@ -1605,4 +1610,31 @@ public void execute_CreateAndGetBalanceJsonpBasedContractGiven_ShouldPutNewAsset // Assert assertThat(result.getFunctionResult().orElseThrow(AssertionError::new)).isEqualTo(argument); } + + @Test + public void create_ValidNamespaceGiven_ShouldCreateNamespace() { + // Arrange + String namespace = "test_namespace"; + NamespaceCreationRequest request = new NamespaceCreationRequest(namespace); + + // Act + assertThatCode(() -> service.create(request)).doesNotThrowAnyException(); + + // Assert + verify(namespaceManager).create(namespace); + } + + @Test + public void create_InvalidNamespaceGiven_ShouldThrowLedgerException() { + // Arrange + String invalidNamespace = "1invalid"; + NamespaceCreationRequest request = new NamespaceCreationRequest(invalidNamespace); + doThrow(LedgerException.class).when(namespaceManager).create(invalidNamespace); + + // Act Assert + assertThatThrownBy(() -> service.create(request)).isInstanceOf(LedgerException.class); + + // Assert + verify(namespaceManager).create(invalidNamespace); + } } diff --git a/ledger/src/main/java/com/scalar/dl/ledger/config/LedgerConfig.java b/ledger/src/main/java/com/scalar/dl/ledger/config/LedgerConfig.java index 4015596b..c325ba7f 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/config/LedgerConfig.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/config/LedgerConfig.java @@ -294,6 +294,7 @@ public String getName() { return name; } + @Override public String getNamespace() { return namespace; } diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/NamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/NamespaceRegistry.java new file mode 100644 index 00000000..f660c417 --- /dev/null +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/NamespaceRegistry.java @@ -0,0 +1,6 @@ +package com.scalar.dl.ledger.database; + +public interface NamespaceRegistry { + + void create(String namespace); +} diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java new file mode 100644 index 00000000..50772650 --- /dev/null +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java @@ -0,0 +1,161 @@ +package com.scalar.dl.ledger.database.scalardb; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.scalar.db.api.Consistency; +import com.scalar.db.api.DistributedStorage; +import com.scalar.db.api.DistributedStorageAdmin; +import com.scalar.db.api.DistributedTransactionAdmin; +import com.scalar.db.api.Get; +import com.scalar.db.api.Put; +import com.scalar.db.api.TableMetadata; +import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.DataType; +import com.scalar.db.io.Key; +import com.scalar.dl.ledger.config.ServerConfig; +import com.scalar.dl.ledger.database.NamespaceRegistry; +import com.scalar.dl.ledger.error.CommonError; +import com.scalar.dl.ledger.exception.DatabaseException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +public abstract class AbstractScalarNamespaceRegistry implements NamespaceRegistry { + @VisibleForTesting static final String NAMESPACE_NAME_SEPARATOR = "_"; + @VisibleForTesting static final String NAMESPACE_TABLE_NAME = "namespace"; + @VisibleForTesting static final String NAMESPACE_COLUMN_NAME = "name"; + + @VisibleForTesting + static final TableMetadata NAMESPACE_TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(NAMESPACE_COLUMN_NAME, DataType.TEXT) + .addPartitionKey(NAMESPACE_COLUMN_NAME) + .build(); + + private final ServerConfig config; + private final DistributedStorage storage; + private final DistributedStorageAdmin storageAdmin; + @Nullable private final DistributedTransactionAdmin transactionAdmin; + private final Set tableMetadataProviders; + private final ImmutableMap storageTables; + private final ImmutableMap transactionTables; + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public AbstractScalarNamespaceRegistry( + ServerConfig config, + DistributedStorage storage, + DistributedStorageAdmin storageAdmin, + DistributedTransactionAdmin transactionAdmin, + Set tableMetadataProviders) { + this.config = checkNotNull(config); + this.storage = checkNotNull(storage); + this.storageAdmin = checkNotNull(storageAdmin); + this.transactionAdmin = transactionAdmin; + this.tableMetadataProviders = checkNotNull(tableMetadataProviders); + this.storageTables = collectStorageTables(); + this.transactionTables = collectTransactionTables(); + } + + public AbstractScalarNamespaceRegistry( + ServerConfig config, + DistributedStorage storage, + DistributedStorageAdmin storageAdmin, + Set tableMetadataProviders) { + this(config, storage, storageAdmin, null, tableMetadataProviders); + } + + @Override + public void create(String namespace) { + createNamespaceManagementTable(); + createNamespace(namespace); + addNamespaceEntry(namespace); + } + + private void createNamespaceManagementTable() { + try { + storageAdmin.createTable( + config.getNamespace(), NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + } catch (ExecutionException e) { + throw new DatabaseException(CommonError.CREATING_NAMESPACE_TABLE_FAILED, e, e.getMessage()); + } + } + + private void createNamespace(String namespace) { + try { + String fullNamespaceName = config.getNamespace() + NAMESPACE_NAME_SEPARATOR + namespace; + storageAdmin.createNamespace(fullNamespaceName, true); + createStorageTables(fullNamespaceName); + createTransactionTables(fullNamespaceName); + } catch (ExecutionException e) { + throw new DatabaseException(CommonError.CREATING_NAMESPACE_FAILED, e, e.getMessage()); + } + } + + private void createStorageTables(String fullNamespaceName) throws ExecutionException { + for (Map.Entry entry : storageTables.entrySet()) { + String tableName = entry.getKey(); + TableMetadata tableMetadata = entry.getValue(); + storageAdmin.createTable(fullNamespaceName, tableName, tableMetadata, true); + } + } + + private void createTransactionTables(String fullNamespaceName) throws ExecutionException { + if (transactionAdmin == null || transactionTables.isEmpty()) { + return; + } + + for (Map.Entry entry : transactionTables.entrySet()) { + String tableName = entry.getKey(); + TableMetadata tableMetadata = entry.getValue(); + transactionAdmin.createTable(fullNamespaceName, tableName, tableMetadata, true); + } + } + + private void addNamespaceEntry(String namespace) { + Get get = + Get.newBuilder() + .namespace(config.getNamespace()) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, namespace)) + .build(); + Put put = + Put.newBuilder() + .namespace(config.getNamespace()) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, namespace)) + .consistency(Consistency.SEQUENTIAL) + .build(); + try { + storage + .get(get) + .ifPresent( + result -> { + throw new DatabaseException(CommonError.NAMESPACE_ALREADY_EXISTS); + }); + storage.put(put); + } catch (ExecutionException e) { + throw new DatabaseException(CommonError.CREATING_NAMESPACE_FAILED, e, e.getMessage()); + } + } + + private ImmutableMap collectStorageTables() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (TableMetadataProvider provider : tableMetadataProviders) { + builder.putAll(provider.getStorageTables()); + } + return builder.build(); + } + + private ImmutableMap collectTransactionTables() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (TableMetadataProvider provider : tableMetadataProviders) { + builder.putAll(provider.getTransactionTables()); + } + return builder.build(); + } +} diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistry.java new file mode 100644 index 00000000..988f3eac --- /dev/null +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistry.java @@ -0,0 +1,21 @@ +package com.scalar.dl.ledger.database.scalardb; + +import com.google.inject.Inject; +import com.scalar.db.api.DistributedStorage; +import com.scalar.db.api.DistributedStorageAdmin; +import com.scalar.db.api.DistributedTransactionAdmin; +import com.scalar.dl.ledger.config.LedgerConfig; +import java.util.Set; + +public class LedgerNamespaceRegistry extends AbstractScalarNamespaceRegistry { + + @Inject + public LedgerNamespaceRegistry( + LedgerConfig config, + DistributedStorage storage, + DistributedStorageAdmin storageAdmin, + DistributedTransactionAdmin transactionAdmin, + Set tableMetadataProviders) { + super(config, storage, storageAdmin, transactionAdmin, tableMetadataProviders); + } +} diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarCertificateRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarCertificateRegistry.java index f49cf0ce..6d1a5318 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarCertificateRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarCertificateRegistry.java @@ -2,6 +2,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.scalar.db.api.Consistency; import com.scalar.db.api.Delete; @@ -9,7 +10,9 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Put; import com.scalar.db.api.Result; +import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import com.scalar.dl.ledger.crypto.CertificateEntry; import com.scalar.dl.ledger.database.CertificateRegistry; @@ -18,11 +21,21 @@ import com.scalar.dl.ledger.exception.MissingCertificateException; import com.scalar.dl.ledger.exception.UnexpectedValueException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Map; import javax.annotation.concurrent.Immutable; @Immutable -public class ScalarCertificateRegistry implements CertificateRegistry { +public class ScalarCertificateRegistry implements CertificateRegistry, TableMetadataProvider { static final String TABLE = "certificate"; + private static final TableMetadata TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(CertificateEntry.ENTITY_ID, DataType.TEXT) + .addColumn(CertificateEntry.VERSION, DataType.INT) + .addColumn(CertificateEntry.PEM, DataType.TEXT) + .addColumn(CertificateEntry.REGISTERED_AT, DataType.BIGINT) + .addPartitionKey(CertificateEntry.ENTITY_ID) + .addClusteringKey(CertificateEntry.VERSION) + .build(); private final DistributedStorage storage; @Inject @@ -31,6 +44,11 @@ public ScalarCertificateRegistry(DistributedStorage storage) { this.storage = checkNotNull(storage); } + @Override + public Map getStorageTables() { + return ImmutableMap.of(TABLE, TABLE_METADATA); + } + @Override public void bind(CertificateEntry entry) { Put put = diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarContractRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarContractRegistry.java index f040486c..8c096964 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarContractRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarContractRegistry.java @@ -2,6 +2,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Streams; import com.google.inject.Inject; import com.scalar.db.api.Consistency; @@ -12,8 +13,10 @@ import com.scalar.db.api.Result; import com.scalar.db.api.Scan; import com.scalar.db.api.Scanner; +import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; import com.scalar.db.exception.storage.NoMutationException; +import com.scalar.db.io.DataType; import com.scalar.db.io.IntValue; import com.scalar.db.io.Key; import com.scalar.db.io.TextValue; @@ -27,17 +30,37 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ThreadSafe -public class ScalarContractRegistry implements ContractRegistry { +public class ScalarContractRegistry implements ContractRegistry, TableMetadataProvider { private static final Logger LOGGER = LoggerFactory.getLogger(ScalarContractRegistry.class.getName()); static final String CONTRACT_TABLE = "contract"; static final String CONTRACT_CLASS_TABLE = "contract_class"; + private static final TableMetadata CONTRACT_TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(ContractEntry.ID, DataType.TEXT) + .addColumn(ContractEntry.ENTITY_ID, DataType.TEXT) + .addColumn(ContractEntry.KEY_VERSION, DataType.INT) + .addColumn(ContractEntry.BINARY_NAME, DataType.TEXT) + .addColumn(ContractEntry.PROPERTIES, DataType.TEXT) + .addColumn(ContractEntry.REGISTERED_AT, DataType.BIGINT) + .addColumn(ContractEntry.SIGNATURE, DataType.BLOB) + .addPartitionKey(ContractEntry.ENTITY_ID) + .addClusteringKey(ContractEntry.KEY_VERSION) + .addClusteringKey(ContractEntry.ID) + .build(); + private static final TableMetadata CONTRACT_CLASS_TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(ContractEntry.BINARY_NAME, DataType.TEXT) + .addColumn(ContractEntry.BYTE_CODE, DataType.BLOB) + .addPartitionKey(ContractEntry.BINARY_NAME) + .build(); private static final int CONTRACT_CACHE_SIZE = 1048576; private static final int CONTRACT_CLASS_CACHE_SIZE = 128; private final DistributedStorage storage; @@ -53,6 +76,15 @@ public ScalarContractRegistry(DistributedStorage storage) { CacheBuilder.newBuilder().maximumSize(CONTRACT_CLASS_CACHE_SIZE).build(); } + @Override + public Map getStorageTables() { + return ImmutableMap.of( + CONTRACT_TABLE, + CONTRACT_TABLE_METADATA, + CONTRACT_CLASS_TABLE, + CONTRACT_CLASS_TABLE_METADATA); + } + @Override public void bind(ContractEntry entry) { if (hasDifferentClassWithSameName(entry)) { diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarFunctionRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarFunctionRegistry.java index 09885169..e08c9f05 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarFunctionRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarFunctionRegistry.java @@ -1,5 +1,6 @@ package com.scalar.dl.ledger.database.scalardb; +import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.scalar.db.api.Consistency; import com.scalar.db.api.Delete; @@ -7,7 +8,9 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Put; import com.scalar.db.api.Result; +import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import com.scalar.dl.ledger.database.FunctionRegistry; import com.scalar.dl.ledger.error.CommonError; @@ -16,10 +19,19 @@ import com.scalar.dl.ledger.exception.UnexpectedValueException; import com.scalar.dl.ledger.function.FunctionEntry; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Map; import java.util.Optional; -public class ScalarFunctionRegistry implements FunctionRegistry { +public class ScalarFunctionRegistry implements FunctionRegistry, TableMetadataProvider { static final String FUNCTION_TABLE = "function"; + private static final TableMetadata FUNCTION_TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(FunctionEntry.ID, DataType.TEXT) + .addColumn(FunctionEntry.BINARY_NAME, DataType.TEXT) + .addColumn(FunctionEntry.BYTE_CODE, DataType.BLOB) + .addColumn(FunctionEntry.REGISTERED_AT, DataType.BIGINT) + .addPartitionKey(FunctionEntry.ID) + .build(); private final DistributedStorage storage; @Inject @@ -28,6 +40,11 @@ public ScalarFunctionRegistry(DistributedStorage storage) { this.storage = storage; } + @Override + public Map getStorageTables() { + return ImmutableMap.of(FUNCTION_TABLE, FUNCTION_TABLE_METADATA); + } + @Override public void bind(FunctionEntry entry) { long currentTime = System.currentTimeMillis(); diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarSecretRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarSecretRegistry.java index cfe5750c..6aea08f2 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarSecretRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarSecretRegistry.java @@ -2,6 +2,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.google.inject.name.Named; import com.scalar.db.api.Consistency; @@ -10,7 +11,9 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Put; import com.scalar.db.api.Result; +import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import com.scalar.dl.ledger.crypto.Cipher; import com.scalar.dl.ledger.crypto.SecretEntry; @@ -21,11 +24,21 @@ import com.scalar.dl.ledger.exception.UnexpectedValueException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.nio.charset.StandardCharsets; +import java.util.Map; import javax.annotation.concurrent.Immutable; @Immutable -public class ScalarSecretRegistry implements SecretRegistry { +public class ScalarSecretRegistry implements SecretRegistry, TableMetadataProvider { static final String TABLE = "secret"; + static final TableMetadata TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(SecretEntry.ENTITY_ID, DataType.TEXT) + .addColumn(SecretEntry.KEY_VERSION, DataType.INT) + .addColumn(SecretEntry.SECRET_KEY, DataType.BLOB) + .addColumn(SecretEntry.REGISTERED_AT, DataType.BIGINT) + .addPartitionKey(SecretEntry.ENTITY_ID) + .addClusteringKey(SecretEntry.KEY_VERSION) + .build(); private final DistributedStorage storage; private final Cipher cipher; @@ -36,6 +49,11 @@ public ScalarSecretRegistry(DistributedStorage storage, @Named("SecretRegistry") this.cipher = cipher; } + @Override + public Map getStorageTables() { + return ImmutableMap.of(TABLE, TABLE_METADATA); + } + @Override public void bind(SecretEntry entry) { byte[] encryptedSecretKey = encrypt(entry.getSecretKey(), entry.getEntityId()); diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTamperEvidentAssetLedger.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTamperEvidentAssetLedger.java index 9aeb0676..edd8597c 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTamperEvidentAssetLedger.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTamperEvidentAssetLedger.java @@ -1,5 +1,6 @@ package com.scalar.dl.ledger.database.scalardb; +import com.google.common.collect.ImmutableMap; import com.scalar.db.api.Consistency; import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.Get; @@ -8,11 +9,13 @@ import com.scalar.db.api.Scan; import com.scalar.db.api.Scan.Ordering; import com.scalar.db.api.Scan.Ordering.Order; +import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.transaction.AbortException; import com.scalar.db.exception.transaction.CommitConflictException; import com.scalar.db.exception.transaction.CommitException; import com.scalar.db.exception.transaction.CrudConflictException; import com.scalar.db.exception.transaction.CrudException; +import com.scalar.db.io.DataType; import com.scalar.db.io.IntValue; import com.scalar.db.io.Key; import com.scalar.db.io.TextValue; @@ -44,6 +47,26 @@ @ThreadSafe public class ScalarTamperEvidentAssetLedger implements TamperEvidentAssetLedger { static final String TABLE = "asset"; + private static final TableMetadata TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(AssetRecord.ID, DataType.TEXT) + .addColumn(AssetRecord.AGE, DataType.INT) + .addColumn(AssetRecord.ARGUMENT, DataType.TEXT) + .addColumn(AssetRecord.CONTRACT_ID, DataType.TEXT) + .addColumn(AssetRecord.HASH, DataType.BLOB) + .addColumn(AssetRecord.INPUT, DataType.TEXT) + .addColumn(AssetRecord.OUTPUT, DataType.TEXT) + .addColumn(AssetRecord.PREV_HASH, DataType.BLOB) + .addColumn(AssetRecord.SIGNATURE, DataType.BLOB) + .addPartitionKey(AssetRecord.ID) + .addClusteringKey(AssetRecord.AGE) + .build(); + private static final TableMetadata METADATA_TABLE_METADATA = + TableMetadata.newBuilder() + .addColumn(AssetMetadata.ID, DataType.TEXT) + .addColumn(AssetMetadata.AGE, DataType.INT) + .addPartitionKey(AssetMetadata.ID) + .build(); private final DistributedTransaction transaction; private final Metadata metadata; private final Snapshot snapshot; @@ -73,6 +96,10 @@ public ScalarTamperEvidentAssetLedger( this.config = config; } + static Map getTransactionTables() { + return ImmutableMap.of(TABLE, TABLE_METADATA, Metadata.TABLE, METADATA_TABLE_METADATA); + } + @Override public Optional get(String assetId) { Optional recordInSnapshot = snapshot.get(assetId); diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTransactionManager.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTransactionManager.java index 2e231b16..d39db965 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTransactionManager.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/ScalarTransactionManager.java @@ -8,6 +8,7 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.transaction.consensuscommit.ConsensusCommitManager; import com.scalar.dl.ledger.config.LedgerConfig; @@ -29,7 +30,7 @@ import org.slf4j.LoggerFactory; @ThreadSafe -public class ScalarTransactionManager implements TransactionManager { +public class ScalarTransactionManager implements TransactionManager, TableMetadataProvider { private static final Logger LOGGER = LoggerFactory.getLogger(ScalarTransactionManager.class.getName()); private final DistributedTransactionManager manager; @@ -53,6 +54,11 @@ public ScalarTransactionManager( this.config = config; } + @Override + public Map getTransactionTables() { + return ScalarTamperEvidentAssetLedger.getTransactionTables(); + } + @Override public Transaction startWith(@Nullable ContractExecutionRequest request) { DistributedTransaction transaction; diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/TableMetadataProvider.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/TableMetadataProvider.java new file mode 100644 index 00000000..36d52297 --- /dev/null +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/TableMetadataProvider.java @@ -0,0 +1,16 @@ +package com.scalar.dl.ledger.database.scalardb; + +import com.google.common.collect.ImmutableMap; +import com.scalar.db.api.TableMetadata; +import java.util.Map; + +public interface TableMetadataProvider { + + default Map getStorageTables() { + return ImmutableMap.of(); + } + + default Map getTransactionTables() { + return ImmutableMap.of(); + } +} diff --git a/ledger/src/main/java/com/scalar/dl/ledger/model/NamespaceCreationRequest.java b/ledger/src/main/java/com/scalar/dl/ledger/model/NamespaceCreationRequest.java new file mode 100644 index 00000000..2ee37f2d --- /dev/null +++ b/ledger/src/main/java/com/scalar/dl/ledger/model/NamespaceCreationRequest.java @@ -0,0 +1,52 @@ +package com.scalar.dl.ledger.model; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; + +@Immutable +public class NamespaceCreationRequest { + @Nonnull private final String namespace; + + /** + * Constructs a {@code NamespaceCreationRequest} with the specified namespace. + * + * @param namespace a namespace name + */ + public NamespaceCreationRequest(String namespace) { + this.namespace = checkNotNull(namespace); + } + + /** + * Returns the namespace name. + * + * @return the namespace name + */ + public String getNamespace() { + return namespace; + } + + /** + * Returns a hash code value for the object. + * + * @return a hash code value for this object. + */ + @Override + public int hashCode() { + return Objects.hash(namespace); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof NamespaceCreationRequest)) { + return false; + } + NamespaceCreationRequest other = (NamespaceCreationRequest) o; + return this.namespace.equals(other.namespace); + } +} diff --git a/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java b/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java new file mode 100644 index 00000000..0e5cc475 --- /dev/null +++ b/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java @@ -0,0 +1,49 @@ +package com.scalar.dl.ledger.namespace; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.inject.Inject; +import com.scalar.dl.ledger.database.NamespaceRegistry; +import com.scalar.dl.ledger.error.CommonError; +import com.scalar.dl.ledger.exception.LedgerException; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +/** + * A manager used to manage namespaces in a {@link NamespaceRegistry}. + * + *

A {@code NamespaceManager} manages namespaces in a {@link NamespaceRegistry}. + */ +public class NamespaceManager { + public static final Pattern NAMESPACE_NAME_PATTERN = Pattern.compile("[a-zA-Z][A-Za-z0-9_]*"); + private final NamespaceRegistry registry; + + /** + * Constructs a {@code NamespaceManager} with the specified {@link NamespaceRegistry}. + * + * @param registry a {@link NamespaceRegistry} + */ + @Inject + public NamespaceManager(NamespaceRegistry registry) { + this.registry = checkNotNull(registry); + } + + /** + * Creates a namespace with the specified name. The namespace name must start with an alphabetic + * character and can only contain alphanumeric characters and underscores. + * + * @param namespace a namespace name to create + * @throws LedgerException if the namespace name is invalid + */ + public void create(@Nonnull String namespace) { + if (!isValidNamespaceName(namespace)) { + throw new LedgerException( + CommonError.INVALID_NAMESPACE_NAME, new IllegalArgumentException(), namespace); + } + registry.create(namespace); + } + + private boolean isValidNamespaceName(@Nonnull String namespace) { + return NAMESPACE_NAME_PATTERN.matcher(namespace).matches(); + } +} diff --git a/ledger/src/main/java/com/scalar/dl/ledger/server/LedgerPrivilegedService.java b/ledger/src/main/java/com/scalar/dl/ledger/server/LedgerPrivilegedService.java index 2b0b9b4a..f5579673 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/server/LedgerPrivilegedService.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/server/LedgerPrivilegedService.java @@ -11,6 +11,7 @@ import com.scalar.dl.rpc.CertificateRegistrationRequest; import com.scalar.dl.rpc.FunctionRegistrationRequest; import com.scalar.dl.rpc.LedgerPrivilegedGrpc; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import com.scalar.dl.rpc.StateRetrievalRequest; import com.scalar.dl.rpc.StateRetrievalResponse; @@ -70,4 +71,11 @@ public void retrieveState( responseObserver.onError(e); } } + + @Override + public void createNamespace( + NamespaceCreationRequest request, StreamObserver responseObserver) { + ThrowableConsumer f = r -> ledger.create(convert(r)); + commonService.serve(f, request, responseObserver); + } } diff --git a/ledger/src/main/java/com/scalar/dl/ledger/server/TypeConverter.java b/ledger/src/main/java/com/scalar/dl/ledger/server/TypeConverter.java index b7e38b16..ef1044a7 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/server/TypeConverter.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/server/TypeConverter.java @@ -11,6 +11,7 @@ import com.scalar.dl.rpc.ExecutionAbortRequest; import com.scalar.dl.rpc.FunctionRegistrationRequest; import com.scalar.dl.rpc.LedgerValidationRequest; +import com.scalar.dl.rpc.NamespaceCreationRequest; import com.scalar.dl.rpc.SecretRegistrationRequest; import com.scalar.dl.rpc.StateRetrievalRequest; import java.util.Base64; @@ -107,6 +108,11 @@ public static com.scalar.dl.ledger.model.ExecutionAbortRequest convert( req.getNonce(), req.getEntityId(), req.getKeyVersion(), req.getSignature().toByteArray()); } + public static com.scalar.dl.ledger.model.NamespaceCreationRequest convert( + NamespaceCreationRequest req) { + return new com.scalar.dl.ledger.model.NamespaceCreationRequest(req.getNamespace()); + } + public static String convert(List entries) { // TODO: revised later JsonObjectBuilder builder = Json.createObjectBuilder(); diff --git a/ledger/src/main/java/com/scalar/dl/ledger/service/BaseService.java b/ledger/src/main/java/com/scalar/dl/ledger/service/BaseService.java index 6f752f20..0e9bee57 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/service/BaseService.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/service/BaseService.java @@ -13,6 +13,8 @@ import com.scalar.dl.ledger.model.CertificateRegistrationRequest; import com.scalar.dl.ledger.model.ContractRegistrationRequest; import com.scalar.dl.ledger.model.ContractsListingRequest; +import com.scalar.dl.ledger.model.NamespaceCreationRequest; +import com.scalar.dl.ledger.namespace.NamespaceManager; import java.util.List; import javax.annotation.concurrent.Immutable; @@ -22,17 +24,20 @@ public class BaseService { private final SecretManager secretManager; private final ClientKeyValidator clientKeyValidator; private final ContractManager contractManager; + private final NamespaceManager namespaceManager; @Inject public BaseService( CertificateManager certManager, SecretManager secretManager, ClientKeyValidator clientKeyValidator, - ContractManager contractManager) { + ContractManager contractManager, + NamespaceManager namespaceManager) { this.certManager = certManager; this.secretManager = secretManager; this.clientKeyValidator = clientKeyValidator; this.contractManager = contractManager; + this.namespaceManager = namespaceManager; } public void register(CertificateRegistrationRequest request) { @@ -59,4 +64,8 @@ public List list(ContractsListingRequest request) { return new ContractScanner(contractManager) .scan(request.getEntityId(), request.getKeyVersion(), request.getContractId()); } + + public void create(NamespaceCreationRequest request) { + namespaceManager.create(request.getNamespace()); + } } diff --git a/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerModule.java b/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerModule.java index 917af66f..f2527e52 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerModule.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerModule.java @@ -1,15 +1,16 @@ package com.scalar.dl.ledger.service; import com.google.inject.AbstractModule; -import com.google.inject.Guice; -import com.google.inject.Injector; import com.google.inject.Provides; import com.google.inject.Singleton; +import com.google.inject.multibindings.Multibinder; import com.scalar.db.api.DistributedStorage; +import com.scalar.db.api.DistributedStorageAdmin; +import com.scalar.db.api.DistributedTransactionAdmin; import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.config.DatabaseConfig; -import com.scalar.db.service.StorageModule; -import com.scalar.db.service.TransactionModule; +import com.scalar.db.service.StorageFactory; +import com.scalar.db.service.TransactionFactory; import com.scalar.dl.ledger.config.AuthenticationMethod; import com.scalar.dl.ledger.config.LedgerConfig; import com.scalar.dl.ledger.config.ServersHmacAuthenticatable; @@ -27,18 +28,22 @@ import com.scalar.dl.ledger.database.CertificateRegistry; import com.scalar.dl.ledger.database.ContractRegistry; import com.scalar.dl.ledger.database.FunctionRegistry; +import com.scalar.dl.ledger.database.NamespaceRegistry; import com.scalar.dl.ledger.database.SecretRegistry; import com.scalar.dl.ledger.database.TransactionManager; import com.scalar.dl.ledger.database.scalardb.DefaultTamperEvidentAssetComposer; +import com.scalar.dl.ledger.database.scalardb.LedgerNamespaceRegistry; import com.scalar.dl.ledger.database.scalardb.ScalarCertificateRegistry; import com.scalar.dl.ledger.database.scalardb.ScalarContractRegistry; import com.scalar.dl.ledger.database.scalardb.ScalarFunctionRegistry; import com.scalar.dl.ledger.database.scalardb.ScalarSecretRegistry; import com.scalar.dl.ledger.database.scalardb.ScalarTransactionManager; +import com.scalar.dl.ledger.database.scalardb.TableMetadataProvider; import com.scalar.dl.ledger.database.scalardb.TamperEvidentAssetComposer; import com.scalar.dl.ledger.database.scalardb.TransactionStateManager; import com.scalar.dl.ledger.function.FunctionLoader; import com.scalar.dl.ledger.function.FunctionManager; +import com.scalar.dl.ledger.namespace.NamespaceManager; import java.net.SocketPermission; import java.security.PermissionCollection; import java.security.Permissions; @@ -77,6 +82,17 @@ protected void configure() { bind(TransactionStateManager.class).in(Singleton.class); bind(ClientKeyValidator.class).in(Singleton.class); bind(AuditorKeyValidator.class).in(Singleton.class); + bind(NamespaceManager.class).in(Singleton.class); + bind(NamespaceRegistry.class).to(LedgerNamespaceRegistry.class).in(Singleton.class); + + // to manage the list of transaction and storage tables for a namespace in Ledger + Multibinder binder = + Multibinder.newSetBinder(binder(), TableMetadataProvider.class); + binder.addBinding().to(ScalarTransactionManager.class); + binder.addBinding().to(ScalarCertificateRegistry.class); + binder.addBinding().to(ScalarSecretRegistry.class); + binder.addBinding().to(ScalarContractRegistry.class); + binder.addBinding().to(ScalarFunctionRegistry.class); } @Provides @@ -113,25 +129,46 @@ SignatureSigner provideSignatureSigner() { } } + @Provides + @Singleton + StorageFactory provideStorageFactory() { + return StorageFactory.create(databaseConfig.getProperties()); + } + @Provides @Singleton DistributedStorage provideDistributedStorage() { - Injector injector = Guice.createInjector(new StorageModule(databaseConfig)); - DistributedStorage storage = injector.getInstance(DistributedStorage.class); + DistributedStorage storage = provideStorageFactory().getStorage(); storage.withNamespace(config.getNamespace()); return storage; } + @Provides + @Singleton + DistributedStorageAdmin provideDistributedStorageAdmin() { + return provideStorageFactory().getStorageAdmin(); + } + + @Provides + @Singleton + TransactionFactory provideTransactionFactory() { + return TransactionFactory.create(databaseConfig.getProperties()); + } + @Provides @Singleton DistributedTransactionManager provideDistributedTransactionManager() { - Injector injector = Guice.createInjector(new TransactionModule(databaseConfig)); - DistributedTransactionManager manager = - injector.getInstance(DistributedTransactionManager.class); + DistributedTransactionManager manager = provideTransactionFactory().getTransactionManager(); manager.withNamespace(config.getNamespace()); return manager; } + @Provides + @Singleton + DistributedTransactionAdmin provideDistributedTransactionAdmin() { + return provideTransactionFactory().getTransactionAdmin(); + } + @Provides @Singleton ProtectionDomain provideProtectionDomain() { diff --git a/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerService.java b/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerService.java index b392d856..9b774492 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerService.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/service/LedgerService.java @@ -22,6 +22,7 @@ import com.scalar.dl.ledger.model.ExecutionAbortRequest; import com.scalar.dl.ledger.model.ExecutionAbortResult; import com.scalar.dl.ledger.model.FunctionRegistrationRequest; +import com.scalar.dl.ledger.model.NamespaceCreationRequest; import com.scalar.dl.ledger.model.StateRetrievalRequest; import com.scalar.dl.ledger.model.StateRetrievalResult; import java.util.List; @@ -107,6 +108,10 @@ public ExecutionAbortResult abort(ExecutionAbortRequest request) { return new ExecutionAbortResult(executor.abort(request.getNonce())); } + public void create(NamespaceCreationRequest request) { + base.create(request); + } + private void validateSignatureFromAuditor(ContractExecutionRequest request) { SignatureValidator validator = auditorKeyValidator.getValidator(); request.validateAuditorSignatureWith(validator); diff --git a/ledger/src/permission-test/java/com/scalar/dl/ledger/service/LedgerServicePermissionTest.java b/ledger/src/permission-test/java/com/scalar/dl/ledger/service/LedgerServicePermissionTest.java index 5640f5a5..e80f9ff4 100644 --- a/ledger/src/permission-test/java/com/scalar/dl/ledger/service/LedgerServicePermissionTest.java +++ b/ledger/src/permission-test/java/com/scalar/dl/ledger/service/LedgerServicePermissionTest.java @@ -30,6 +30,7 @@ import com.scalar.dl.ledger.database.TransactionManager; import com.scalar.dl.ledger.function.FunctionManager; import com.scalar.dl.ledger.model.ContractExecutionRequest; +import com.scalar.dl.ledger.namespace.NamespaceManager; import com.scalar.dl.ledger.statemachine.DeprecatedLedger; import com.scalar.dl.ledger.statemachine.DeserializationType; import java.io.File; @@ -81,6 +82,7 @@ public class LedgerServicePermissionTest { @Mock private FunctionManager functionManager; @Mock private ClientKeyValidator clientKeyValidator; @Mock private AuditorKeyValidator auditorKeyValidator; + @Mock private NamespaceManager namespaceManager; private LedgerService service; @BeforeEach @@ -105,7 +107,8 @@ public void setUp() { new ContractExecutor(config, contractManager, functionManager, transactionManager); service = new LedgerService( - new BaseService(certManager, secretManager, clientKeyValidator, contractManager), + new BaseService( + certManager, secretManager, clientKeyValidator, contractManager, namespaceManager), config, clientKeyValidator, auditorKeyValidator, diff --git a/ledger/src/test/java/com/scalar/dl/ledger/config/LedgerConfigTest.java b/ledger/src/test/java/com/scalar/dl/ledger/config/LedgerConfigTest.java index 9a376dc7..91a7ae78 100644 --- a/ledger/src/test/java/com/scalar/dl/ledger/config/LedgerConfigTest.java +++ b/ledger/src/test/java/com/scalar/dl/ledger/config/LedgerConfigTest.java @@ -19,7 +19,6 @@ public class LedgerConfigTest { private static final String SOME_NUMBER = "9999"; private static final String SOME_PATH = "/dev/null"; private static final String SOME_PEM = "-- PEM --"; - private static final String SOME_KEY = "key"; private static final String SOME_NAME = "My Ledger"; private static final String SOME_SECRET_KEY = "secret-key"; private static final String SOME_CIPHER_KEY = "cipher-key"; diff --git a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java new file mode 100644 index 00000000..cd51f205 --- /dev/null +++ b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java @@ -0,0 +1,209 @@ +package com.scalar.dl.ledger.database.scalardb; + +import static com.scalar.dl.ledger.database.scalardb.AbstractScalarNamespaceRegistry.NAMESPACE_COLUMN_NAME; +import static com.scalar.dl.ledger.database.scalardb.AbstractScalarNamespaceRegistry.NAMESPACE_NAME_SEPARATOR; +import static com.scalar.dl.ledger.database.scalardb.AbstractScalarNamespaceRegistry.NAMESPACE_TABLE_METADATA; +import static com.scalar.dl.ledger.database.scalardb.AbstractScalarNamespaceRegistry.NAMESPACE_TABLE_NAME; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.scalar.db.api.DistributedStorage; +import com.scalar.db.api.DistributedStorageAdmin; +import com.scalar.db.api.DistributedTransactionAdmin; +import com.scalar.db.api.Get; +import com.scalar.db.api.Put; +import com.scalar.db.api.Result; +import com.scalar.db.api.TableMetadata; +import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.Key; +import com.scalar.dl.ledger.config.LedgerConfig; +import com.scalar.dl.ledger.error.CommonError; +import com.scalar.dl.ledger.exception.DatabaseException; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +public class LedgerNamespaceRegistryTest { + private static final String SOME_DEFAULT_NAMESPACE = "scalar"; + private static final String SOME_NAMESPACE = "some_namespace"; + private static final String SOME_TABLE_NAME_1 = "some_table_name_1"; + private static final String SOME_TABLE_NAME_2 = "some_table_name_2"; + private static final String SOME_TABLE_NAME_3 = "some_table_name_3"; + private static final String SOME_TABLE_NAME_4 = "some_table_name_4"; + @Mock private LedgerConfig config; + @Mock private DistributedStorage storage; + @Mock private DistributedStorageAdmin storageAdmin; + @Mock private DistributedTransactionAdmin transactionAdmin; + @Mock private ScalarTransactionManager transactionManager; + @Mock private ScalarCertificateRegistry certificateRegistry; + @Mock private ScalarSecretRegistry secretRegistry; + @Mock private TableMetadata tableMetadata1; + @Mock private TableMetadata tableMetadata2; + @Mock private TableMetadata tableMetadata3; + @Mock private TableMetadata tableMetadata4; + private LedgerNamespaceRegistry namespaceRegistry; + private AutoCloseable closeable; + + @BeforeEach + public void setUp() { + closeable = openMocks(this); + when(transactionManager.getTransactionTables()) + .thenReturn( + ImmutableMap.of(SOME_TABLE_NAME_1, tableMetadata1, SOME_TABLE_NAME_2, tableMetadata2)); + when(certificateRegistry.getStorageTables()) + .thenReturn(ImmutableMap.of(SOME_TABLE_NAME_3, tableMetadata3)); + when(secretRegistry.getStorageTables()) + .thenReturn(ImmutableMap.of(SOME_TABLE_NAME_4, tableMetadata4)); + namespaceRegistry = + new LedgerNamespaceRegistry( + config, + storage, + storageAdmin, + transactionAdmin, + ImmutableSet.of(transactionManager, certificateRegistry, secretRegistry)); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); + } + + @Test + public void create_NewNamespaceGiven_ShouldCreateProperly() throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Get expectedGet = + Get.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .build(); + Put expectedPut = + Put.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .build(); + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + + // Act + namespaceRegistry.create(SOME_NAMESPACE); + + // Assert + verify(storageAdmin).createNamespace(fullNamespace, true); + verify(storageAdmin) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage).get(expectedGet); + verify(storage).put(expectedPut); + } + + @Test + public void create_ExistingNamespaceGiven_ShouldThrowException() throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Result result = Mockito.mock(Result.class); + when(storage.get(any(Get.class))).thenReturn(Optional.of(result)); + Get expectedGet = + Get.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .build(); + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + + // Act Assert + assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) + .isInstanceOf(DatabaseException.class) + .hasMessageContaining(CommonError.NAMESPACE_ALREADY_EXISTS.getMessage()); + verify(storageAdmin).createNamespace(fullNamespace, true); + verify(storageAdmin) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage).get(expectedGet); + verify(storage, never()).put(any(Put.class)); + } + + @Test + public void create_CreateNamespaceManagementTableFailed_ShouldThrowDatabaseException() + throws ExecutionException { + // Arrange + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + ExecutionException toThrow = new ExecutionException("details"); + doThrow(toThrow) + .when(storageAdmin) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + + // Act Assert + assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) + .isInstanceOf(DatabaseException.class) + .hasCause(toThrow) + .hasMessageContaining(CommonError.CREATING_NAMESPACE_TABLE_FAILED.buildMessage("details")); + verify(storageAdmin) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin, never()).createNamespace(any(), anyBoolean()); + } + + @Test + public void create_CreateNamespaceFailed_ShouldThrowDatabaseException() + throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + ExecutionException toThrow = new ExecutionException("details"); + doThrow(toThrow).when(storageAdmin).createNamespace(fullNamespace, true); + + // Act Assert + assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) + .isInstanceOf(DatabaseException.class) + .hasCause(toThrow) + .hasMessageContaining(CommonError.CREATING_NAMESPACE_FAILED.buildMessage("details")); + verify(storageAdmin) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin).createNamespace(fullNamespace, true); + verify(storage, never()).get(any(Get.class)); + verify(storage, never()).put(any(Put.class)); + } + + @Test + public void create_AddNamespaceEntryFailed_ShouldThrowDatabaseException() + throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + ExecutionException toThrow = new ExecutionException("details"); + doThrow(toThrow).when(storage).get(any(Get.class)); + + // Act Assert + assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) + .isInstanceOf(DatabaseException.class) + .hasCause(toThrow) + .hasMessageContaining(CommonError.CREATING_NAMESPACE_FAILED.buildMessage("details")); + verify(storageAdmin) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin).createNamespace(fullNamespace, true); + verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage).get(any(Get.class)); + verify(storage, never()).put(any(Put.class)); + } +} diff --git a/ledger/src/test/java/com/scalar/dl/ledger/model/NamespaceCreationRequestTest.java b/ledger/src/test/java/com/scalar/dl/ledger/model/NamespaceCreationRequestTest.java new file mode 100644 index 00000000..57a94465 --- /dev/null +++ b/ledger/src/test/java/com/scalar/dl/ledger/model/NamespaceCreationRequestTest.java @@ -0,0 +1,119 @@ +package com.scalar.dl.ledger.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.jupiter.api.Test; + +public class NamespaceCreationRequestTest { + private static final String NAMESPACE = "test_namespace"; + + @Test + public void constructor_ProperValueGiven_ShouldInstantiate() { + // Act Assert + assertThatCode(() -> new NamespaceCreationRequest(NAMESPACE)).doesNotThrowAnyException(); + } + + @Test + public void getNamespace_ProperValueGiven_ShouldReturnProperValue() { + // Act + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + + // Assert + assertThat(request.getNamespace()).isEqualTo(NAMESPACE); + } + + @Test + public void equals_OnTheSameObject_ShouldReturnTrue() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + NamespaceCreationRequest other = request; + + // Act + boolean result = request.equals(other); + + // Assert + assertThat(result).isTrue(); + } + + @Test + public void equals_OnTheSameData_ShouldReturnTrue() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + NamespaceCreationRequest other = new NamespaceCreationRequest(NAMESPACE); + + // Act + boolean result = request.equals(other); + + // Assert + assertThat(result).isTrue(); + } + + @Test + public void equals_OnDifferentData_ShouldReturnFalse() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + String differentNamespace = "different_namespace"; + NamespaceCreationRequest other = new NamespaceCreationRequest(differentNamespace); + + // Act + boolean result = request.equals(other); + + // Assert + assertThat(result).isFalse(); + } + + @Test + public void equals_OnNull_ShouldReturnFalse() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + + // Act + boolean result = request.equals(null); + + // Assert + assertThat(result).isFalse(); + } + + @Test + public void equals_OnAnArbitraryObject_ShouldReturnFalse() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + Object arbitraryObject = new Object(); + + // Act + boolean result = request.equals(arbitraryObject); + + // Assert + assertThat(result).isFalse(); + } + + @Test + public void hashCode_OnTheSameData_ShouldReturnSameHashCode() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + NamespaceCreationRequest other = new NamespaceCreationRequest(NAMESPACE); + + // Act + int hashCode1 = request.hashCode(); + int hashCode2 = other.hashCode(); + + // Assert + assertThat(hashCode1).isEqualTo(hashCode2); + } + + @Test + public void hashCode_OnDifferentData_ShouldReturnDifferentHashCode() { + // Arrange + NamespaceCreationRequest request = new NamespaceCreationRequest(NAMESPACE); + String differentNamespace = "different_namespace"; + NamespaceCreationRequest other = new NamespaceCreationRequest(differentNamespace); + + // Act + int hashCode1 = request.hashCode(); + int hashCode2 = other.hashCode(); + + // Assert + assertThat(hashCode1).isNotEqualTo(hashCode2); + } +} diff --git a/ledger/src/test/java/com/scalar/dl/ledger/namespace/NamespaceManagerTest.java b/ledger/src/test/java/com/scalar/dl/ledger/namespace/NamespaceManagerTest.java new file mode 100644 index 00000000..453f9f5a --- /dev/null +++ b/ledger/src/test/java/com/scalar/dl/ledger/namespace/NamespaceManagerTest.java @@ -0,0 +1,126 @@ +package com.scalar.dl.ledger.namespace; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import com.scalar.dl.ledger.database.NamespaceRegistry; +import com.scalar.dl.ledger.error.CommonError; +import com.scalar.dl.ledger.exception.LedgerException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class NamespaceManagerTest { + @Mock private NamespaceRegistry registry; + private NamespaceManager manager; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + manager = new NamespaceManager(registry); + } + + @Test + public void create_ValidNamespaceNameGiven_ShouldCreateNamespace() { + // Arrange + String namespace = "valid_namespace"; + + // Act + assertThatCode(() -> manager.create(namespace)).doesNotThrowAnyException(); + + // Assert + verify(registry).create(namespace); + } + + @Test + public void create_ValidNamespaceWithAlphanumericAndUnderscore_ShouldCreateNamespace() { + // Arrange + String namespace = "a123_ABC"; + + // Act + assertThatCode(() -> manager.create(namespace)).doesNotThrowAnyException(); + + // Assert + verify(registry).create(namespace); + } + + @Test + public void create_ValidNamespaceWithMinimumLength_ShouldCreateNamespace() { + // Arrange + String namespace = "a"; + + // Act + assertThatCode(() -> manager.create(namespace)).doesNotThrowAnyException(); + + // Assert + verify(registry).create(namespace); + } + + @Test + public void create_NamespaceStartingWithNumber_ShouldThrowLedgerException() { + // Arrange + String namespace = "1invalid"; + + // Act Assert + assertThatThrownBy(() -> manager.create(namespace)) + .isInstanceOf(LedgerException.class) + .hasMessage(CommonError.INVALID_NAMESPACE_NAME.buildMessage(namespace)); + + verify(registry, never()).create(namespace); + } + + @Test + public void create_NamespaceWithHyphen_ShouldThrowLedgerException() { + // Arrange + String namespace = "invalid-name"; + + // Act Assert + assertThatThrownBy(() -> manager.create(namespace)) + .isInstanceOf(LedgerException.class) + .hasMessage(CommonError.INVALID_NAMESPACE_NAME.buildMessage(namespace)); + + verify(registry, never()).create(namespace); + } + + @Test + public void create_NamespaceWithSpecialCharacter_ShouldThrowLedgerException() { + // Arrange + String namespace = "invalid@name"; + + // Act Assert + assertThatThrownBy(() -> manager.create(namespace)) + .isInstanceOf(LedgerException.class) + .hasMessage(CommonError.INVALID_NAMESPACE_NAME.buildMessage(namespace)); + + verify(registry, never()).create(namespace); + } + + @Test + public void create_EmptyNamespace_ShouldThrowLedgerException() { + // Arrange + String namespace = ""; + + // Act Assert + assertThatThrownBy(() -> manager.create(namespace)) + .isInstanceOf(LedgerException.class) + .hasMessage(CommonError.INVALID_NAMESPACE_NAME.buildMessage(namespace)); + + verify(registry, never()).create(namespace); + } + + @Test + public void create_NamespaceStartingWithUnderscore_ShouldThrowLedgerException() { + // Arrange + String namespace = "_invalid"; + + // Act Assert + assertThatThrownBy(() -> manager.create(namespace)) + .isInstanceOf(LedgerException.class) + .hasMessage(CommonError.INVALID_NAMESPACE_NAME.buildMessage(namespace)); + + verify(registry, never()).create(namespace); + } +} diff --git a/ledger/src/test/java/com/scalar/dl/ledger/service/BaseServiceTest.java b/ledger/src/test/java/com/scalar/dl/ledger/service/BaseServiceTest.java index 4bfe6984..452956d3 100644 --- a/ledger/src/test/java/com/scalar/dl/ledger/service/BaseServiceTest.java +++ b/ledger/src/test/java/com/scalar/dl/ledger/service/BaseServiceTest.java @@ -24,6 +24,8 @@ import com.scalar.dl.ledger.model.CertificateRegistrationRequest; import com.scalar.dl.ledger.model.ContractRegistrationRequest; import com.scalar.dl.ledger.model.ContractsListingRequest; +import com.scalar.dl.ledger.model.NamespaceCreationRequest; +import com.scalar.dl.ledger.namespace.NamespaceManager; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @@ -39,10 +41,12 @@ public class BaseServiceTest { @Mock private SecretManager secretManager; @Mock private ClientKeyValidator clientKeyValidator; @Mock private ContractManager contractManager; + @Mock private NamespaceManager namespaceManager; @Mock private CertificateRegistrationRequest certRegistrationRequest; @Mock private SecretEntry secretEntry; @Mock private ContractRegistrationRequest contractRegistrationRequest; @Mock private ContractsListingRequest contractsListingRequest; + @Mock private NamespaceCreationRequest namespaceCreationRequest; @Mock private DigitalSignatureValidator validator; @InjectMocks private BaseService service; @@ -53,6 +57,7 @@ public class BaseServiceTest { private static final int SOME_KEY_VERSION = 1; private static final byte[] SOME_SIGNATURE = "signature".getBytes(StandardCharsets.UTF_8); private static final String SOME_PEM = "pem"; + private static final String SOME_NAMESPACE = "test_namespace"; @BeforeEach public void setUp() { @@ -93,6 +98,10 @@ private void configureRequestValidation(AbstractRequest request, boolean isValid } } + private void configureNamespaceCreationRequest(NamespaceCreationRequest request) { + when(request.getNamespace()).thenReturn(SOME_NAMESPACE); + } + private ContractEntry prepareContractEntry(String contractId) { ContractEntry entry = mock(ContractEntry.class); when(entry.getId()).thenReturn(contractId); @@ -188,4 +197,17 @@ public void list_ProperListingRequestWithoutContractIdGiven_ShouldListContracts( .scan(contractsListingRequest.getEntityId(), contractsListingRequest.getKeyVersion()); assertThat(actual).containsExactly(entry1, entry2); } + + @Test + public void create_ProperNamespaceCreationRequestGiven_ShouldCreateNamespace() { + // Arrange + configureNamespaceCreationRequest(namespaceCreationRequest); + doNothing().when(namespaceManager).create(SOME_NAMESPACE); + + // Act + service.create(namespaceCreationRequest); + + // Assert + verify(namespaceManager).create(SOME_NAMESPACE); + } } diff --git a/rpc/src/main/proto/scalar.proto b/rpc/src/main/proto/scalar.proto index 95ef1bcd..3bc6c914 100644 --- a/rpc/src/main/proto/scalar.proto +++ b/rpc/src/main/proto/scalar.proto @@ -34,6 +34,8 @@ service LedgerPrivileged { } rpc RetrieveState (StateRetrievalRequest) returns (StateRetrievalResponse) { } + rpc createNamespace (NamespaceCreationRequest) returns (google.protobuf.Empty) { + } } service Auditor { @@ -52,6 +54,8 @@ service AuditorPrivileged { } rpc RegisterSecret (SecretRegistrationRequest) returns (google.protobuf.Empty) { } + rpc createNamespace (NamespaceCreationRequest) returns (google.protobuf.Empty) { + } } service Gateway { @@ -72,6 +76,8 @@ service GatewayPrivileged { } rpc RegisterFunction (FunctionRegistrationRequest) returns (google.protobuf.Empty) { } + rpc createNamespace (NamespaceCreationRequest) returns (google.protobuf.Empty) { + } } message CertificateRegistrationRequest { @@ -159,6 +165,10 @@ message ContractsListingResponse { string json = 1; } +message NamespaceCreationRequest { + string namespace = 1; +} + message ContractExecutionResponse { string contract_result = 1; // the result of contract execution repeated AssetProof proofs = 2; // proofs given from the ledger server From b6cda098fdfa60c8c93861138102b563a7a91257 Mon Sep 17 00:00:00 2001 From: Jun Nemoto Date: Mon, 10 Nov 2025 15:46:00 +0900 Subject: [PATCH 2/5] Revise based on feedback from AI --- .../scalar/dl/ledger/error/CommonError.java | 2 +- .../AbstractScalarNamespaceRegistry.java | 16 +++--------- .../dl/ledger/namespace/NamespaceManager.java | 5 ++-- .../scalardb/LedgerNamespaceRegistryTest.java | 26 ++++++++++--------- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java b/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java index c15326da..bba55e53 100644 --- a/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java +++ b/common/src/main/java/com/scalar/dl/ledger/error/CommonError.java @@ -223,7 +223,7 @@ public enum CommonError implements ScalarDlError { StatusCode.SECRET_NOT_FOUND, "001", "The specified secret is not found.", "", ""), // - // Errors for SECRET_ALREADY_REGISTERED(416) + // Errors for NAMESPACE_ALREADY_EXISTS(416) // NAMESPACE_ALREADY_EXISTS( StatusCode.NAMESPACE_ALREADY_EXISTS, diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java index 50772650..511aa987 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java @@ -8,10 +8,10 @@ import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.DistributedTransactionAdmin; -import com.scalar.db.api.Get; import com.scalar.db.api.Put; import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.exception.storage.NoMutationException; import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import com.scalar.dl.ledger.config.ServerConfig; @@ -117,12 +117,6 @@ private void createTransactionTables(String fullNamespaceName) throws ExecutionE } private void addNamespaceEntry(String namespace) { - Get get = - Get.newBuilder() - .namespace(config.getNamespace()) - .table(NAMESPACE_TABLE_NAME) - .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, namespace)) - .build(); Put put = Put.newBuilder() .namespace(config.getNamespace()) @@ -131,13 +125,9 @@ private void addNamespaceEntry(String namespace) { .consistency(Consistency.SEQUENTIAL) .build(); try { - storage - .get(get) - .ifPresent( - result -> { - throw new DatabaseException(CommonError.NAMESPACE_ALREADY_EXISTS); - }); storage.put(put); + } catch (NoMutationException e) { + throw new DatabaseException(CommonError.NAMESPACE_ALREADY_EXISTS); } catch (ExecutionException e) { throw new DatabaseException(CommonError.CREATING_NAMESPACE_FAILED, e, e.getMessage()); } diff --git a/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java b/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java index 0e5cc475..866ac275 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/namespace/NamespaceManager.java @@ -15,7 +15,7 @@ *

A {@code NamespaceManager} manages namespaces in a {@link NamespaceRegistry}. */ public class NamespaceManager { - public static final Pattern NAMESPACE_NAME_PATTERN = Pattern.compile("[a-zA-Z][A-Za-z0-9_]*"); + public static final Pattern NAMESPACE_NAME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9_]*"); private final NamespaceRegistry registry; /** @@ -37,8 +37,7 @@ public NamespaceManager(NamespaceRegistry registry) { */ public void create(@Nonnull String namespace) { if (!isValidNamespaceName(namespace)) { - throw new LedgerException( - CommonError.INVALID_NAMESPACE_NAME, new IllegalArgumentException(), namespace); + throw new LedgerException(CommonError.INVALID_NAMESPACE_NAME, namespace); } registry.create(namespace); } diff --git a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java index cd51f205..9fc964a5 100644 --- a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java +++ b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java @@ -20,14 +20,13 @@ import com.scalar.db.api.DistributedTransactionAdmin; import com.scalar.db.api.Get; import com.scalar.db.api.Put; -import com.scalar.db.api.Result; import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.exception.storage.NoMutationException; import com.scalar.db.io.Key; import com.scalar.dl.ledger.config.LedgerConfig; import com.scalar.dl.ledger.error.CommonError; import com.scalar.dl.ledger.exception.DatabaseException; -import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,7 +107,6 @@ public void create_NewNamespaceGiven_ShouldCreateProperly() throws ExecutionExce verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); - verify(storage).get(expectedGet); verify(storage).put(expectedPut); } @@ -116,15 +114,15 @@ public void create_NewNamespaceGiven_ShouldCreateProperly() throws ExecutionExce public void create_ExistingNamespaceGiven_ShouldThrowException() throws ExecutionException { // Arrange String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; - Result result = Mockito.mock(Result.class); - when(storage.get(any(Get.class))).thenReturn(Optional.of(result)); - Get expectedGet = - Get.newBuilder() + Put expectedPut = + Put.newBuilder() .namespace(SOME_DEFAULT_NAMESPACE) .table(NAMESPACE_TABLE_NAME) .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) .build(); when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + NoMutationException toThrow = Mockito.mock(NoMutationException.class); + doThrow(toThrow).when(storage).put(any(Put.class)); // Act Assert assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) @@ -137,8 +135,7 @@ public void create_ExistingNamespaceGiven_ShouldThrowException() throws Executio verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); - verify(storage).get(expectedGet); - verify(storage, never()).put(any(Put.class)); + verify(storage).put(expectedPut); } @Test @@ -187,9 +184,15 @@ public void create_AddNamespaceEntryFailed_ShouldThrowDatabaseException() throws ExecutionException { // Arrange String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Put expectedPut = + Put.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .build(); when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); ExecutionException toThrow = new ExecutionException("details"); - doThrow(toThrow).when(storage).get(any(Get.class)); + doThrow(toThrow).when(storage).put(expectedPut); // Act Assert assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) @@ -203,7 +206,6 @@ public void create_AddNamespaceEntryFailed_ShouldThrowDatabaseException() verify(transactionAdmin).createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); - verify(storage).get(any(Get.class)); - verify(storage, never()).put(any(Put.class)); + verify(storage).put(expectedPut); } } From 9cf3596237a9aa383793b9282ba9e9c4fd3dc606 Mon Sep 17 00:00:00 2001 From: Jun Nemoto Date: Mon, 10 Nov 2025 16:16:01 +0900 Subject: [PATCH 3/5] Fix --- .../com/scalar/dl/ledger/server/CommonService.java | 1 + .../scalardb/AbstractScalarNamespaceRegistry.java | 4 ++-- .../database/scalardb/LedgerNamespaceRegistryTest.java | 10 ++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/com/scalar/dl/ledger/server/CommonService.java b/common/src/main/java/com/scalar/dl/ledger/server/CommonService.java index 51be66bf..166b18cb 100644 --- a/common/src/main/java/com/scalar/dl/ledger/server/CommonService.java +++ b/common/src/main/java/com/scalar/dl/ledger/server/CommonService.java @@ -139,6 +139,7 @@ private io.grpc.Status convert(StatusCode code) { case CERTIFICATE_ALREADY_REGISTERED: case SECRET_ALREADY_REGISTERED: case CONTRACT_ALREADY_REGISTERED: + case NAMESPACE_ALREADY_EXISTS: return io.grpc.Status.ALREADY_EXISTS; case UNAVAILABLE: return io.grpc.Status.UNAVAILABLE; diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java index 511aa987..3f6f38f1 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java @@ -4,7 +4,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import com.scalar.db.api.Consistency; +import com.scalar.db.api.ConditionBuilder; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.DistributedTransactionAdmin; @@ -122,7 +122,7 @@ private void addNamespaceEntry(String namespace) { .namespace(config.getNamespace()) .table(NAMESPACE_TABLE_NAME) .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, namespace)) - .consistency(Consistency.SEQUENTIAL) + .condition(ConditionBuilder.putIfNotExists()) .build(); try { storage.put(put); diff --git a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java index 9fc964a5..d32d0c85 100644 --- a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java +++ b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java @@ -15,6 +15,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.scalar.db.api.ConditionBuilder; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.DistributedTransactionAdmin; @@ -82,17 +83,12 @@ public void tearDown() throws Exception { public void create_NewNamespaceGiven_ShouldCreateProperly() throws ExecutionException { // Arrange String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; - Get expectedGet = - Get.newBuilder() - .namespace(SOME_DEFAULT_NAMESPACE) - .table(NAMESPACE_TABLE_NAME) - .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) - .build(); Put expectedPut = Put.newBuilder() .namespace(SOME_DEFAULT_NAMESPACE) .table(NAMESPACE_TABLE_NAME) .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) .build(); when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); @@ -119,6 +115,7 @@ public void create_ExistingNamespaceGiven_ShouldThrowException() throws Executio .namespace(SOME_DEFAULT_NAMESPACE) .table(NAMESPACE_TABLE_NAME) .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) .build(); when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); NoMutationException toThrow = Mockito.mock(NoMutationException.class); @@ -189,6 +186,7 @@ public void create_AddNamespaceEntryFailed_ShouldThrowDatabaseException() .namespace(SOME_DEFAULT_NAMESPACE) .table(NAMESPACE_TABLE_NAME) .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) .build(); when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); ExecutionException toThrow = new ExecutionException("details"); From 7b30e384f546f0f33eab10592d7e5279fd4b3936 Mon Sep 17 00:00:00 2001 From: Jun Nemoto Date: Mon, 17 Nov 2025 14:15:24 +0900 Subject: [PATCH 4/5] Add retry logic --- .../AbstractScalarNamespaceRegistry.java | 40 ++++- .../scalardb/LedgerNamespaceRegistryTest.java | 143 ++++++++++++++++++ 2 files changed, 180 insertions(+), 3 deletions(-) diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java index 3f6f38f1..148a28e2 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java @@ -4,12 +4,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Uninterruptibles; import com.scalar.db.api.ConditionBuilder; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.DistributedTransactionAdmin; import com.scalar.db.api.Put; import com.scalar.db.api.TableMetadata; +import com.scalar.db.common.CoreError; import com.scalar.db.exception.storage.ExecutionException; import com.scalar.db.exception.storage.NoMutationException; import com.scalar.db.io.DataType; @@ -19,13 +21,23 @@ import com.scalar.dl.ledger.error.CommonError; import com.scalar.dl.ledger.exception.DatabaseException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Immutable public abstract class AbstractScalarNamespaceRegistry implements NamespaceRegistry { + private static final Logger LOGGER = + LoggerFactory.getLogger(AbstractScalarNamespaceRegistry.class.getName()); + private static final int MAX_ATTEMPTS = 5; + private static final long SLEEP_BASE_MILLIS = 100L; @VisibleForTesting static final String NAMESPACE_NAME_SEPARATOR = "_"; @VisibleForTesting static final String NAMESPACE_TABLE_NAME = "namespace"; @VisibleForTesting static final String NAMESPACE_COLUMN_NAME = "name"; @@ -44,6 +56,7 @@ public abstract class AbstractScalarNamespaceRegistry implements NamespaceRegist private final Set tableMetadataProviders; private final ImmutableMap storageTables; private final ImmutableMap transactionTables; + private final Retry retry; @SuppressFBWarnings("EI_EXPOSE_REP2") public AbstractScalarNamespaceRegistry( @@ -59,6 +72,20 @@ public AbstractScalarNamespaceRegistry( this.tableMetadataProviders = checkNotNull(tableMetadataProviders); this.storageTables = collectStorageTables(); this.transactionTables = collectTransactionTables(); + this.retry = + Retry.of( + "retry", + RetryConfig.custom() + .maxAttempts(MAX_ATTEMPTS) + .intervalFunction(IntervalFunction.ofExponentialBackoff(SLEEP_BASE_MILLIS, 2.0)) + .retryOnException( + e -> { + LOGGER.warn("Namespace creation failed"); + return e instanceof IllegalArgumentException + && (e.getMessage().startsWith(CoreError.NAMESPACE_NOT_FOUND.buildCode()) + || e.getMessage().startsWith(CoreError.TABLE_NOT_FOUND.buildCode())); + }) + .build()); } public AbstractScalarNamespaceRegistry( @@ -71,9 +98,14 @@ public AbstractScalarNamespaceRegistry( @Override public void create(String namespace) { - createNamespaceManagementTable(); - createNamespace(namespace); - addNamespaceEntry(namespace); + Retry.decorateRunnable( + retry, + () -> { + createNamespaceManagementTable(); + createNamespace(namespace); + addNamespaceEntry(namespace); + }) + .run(); } private void createNamespaceManagementTable() { @@ -89,6 +121,8 @@ private void createNamespace(String namespace) { try { String fullNamespaceName = config.getNamespace() + NAMESPACE_NAME_SEPARATOR + namespace; storageAdmin.createNamespace(fullNamespaceName, true); + // this sleep can be removed after negative cache is disabled in ScalarDB + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); createStorageTables(fullNamespaceName); createTransactionTables(fullNamespaceName); } catch (ExecutionException e) { diff --git a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java index d32d0c85..2988bb00 100644 --- a/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java +++ b/ledger/src/test/java/com/scalar/dl/ledger/database/scalardb/LedgerNamespaceRegistryTest.java @@ -9,6 +9,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; @@ -22,6 +23,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Put; import com.scalar.db.api.TableMetadata; +import com.scalar.db.common.CoreError; import com.scalar.db.exception.storage.ExecutionException; import com.scalar.db.exception.storage.NoMutationException; import com.scalar.db.io.Key; @@ -206,4 +208,145 @@ public void create_AddNamespaceEntryFailed_ShouldThrowDatabaseException() verify(storageAdmin).createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); verify(storage).put(expectedPut); } + + @Test + public void create_AddNamespaceEntryFailedDueToTableNotFound_ShouldRetry() + throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Put expectedPut = + Put.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) + .build(); + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + IllegalArgumentException toThrow = + new IllegalArgumentException(CoreError.TABLE_NOT_FOUND.buildMessage(NAMESPACE_TABLE_NAME)); + doThrow(toThrow).doNothing().when(storage).put(expectedPut); + + // Act + namespaceRegistry.create(SOME_NAMESPACE); + + // Assert + verify(storageAdmin, times(2)) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin, times(2)).createNamespace(fullNamespace, true); + verify(transactionAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage, times(2)).put(expectedPut); + } + + @Test + public void create_AddNamespaceEntryFailedDueToNamespaceNotFound_ShouldRetry() + throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Put expectedPut = + Put.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) + .build(); + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + IllegalArgumentException toThrow = + new IllegalArgumentException( + CoreError.NAMESPACE_NOT_FOUND.buildMessage(SOME_DEFAULT_NAMESPACE)); + doThrow(toThrow).doNothing().when(storage).put(expectedPut); + + // Act + namespaceRegistry.create(SOME_NAMESPACE); + + // Assert + verify(storageAdmin, times(2)) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin, times(2)).createNamespace(fullNamespace, true); + verify(transactionAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin, times(2)) + .createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage, times(2)).put(expectedPut); + } + + @Test + public void create_MaxAttemptsExceeded_ShouldThrowDatabaseException() throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Put expectedPut = + Put.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) + .build(); + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + IllegalArgumentException toThrow = + new IllegalArgumentException(CoreError.TABLE_NOT_FOUND.buildMessage(NAMESPACE_TABLE_NAME)); + doThrow(toThrow).when(storage).put(expectedPut); + + // Act Assert + assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(CoreError.TABLE_NOT_FOUND.buildMessage(NAMESPACE_TABLE_NAME)); + verify(storageAdmin, times(5)) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin, times(5)).createNamespace(fullNamespace, true); + verify(transactionAdmin, times(5)) + .createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin, times(5)) + .createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin, times(5)) + .createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin, times(5)) + .createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage, times(5)).put(expectedPut); + } + + @Test + public void create_NonRetryableException_ShouldNotRetry() throws ExecutionException { + // Arrange + String fullNamespace = SOME_DEFAULT_NAMESPACE + NAMESPACE_NAME_SEPARATOR + SOME_NAMESPACE; + Put expectedPut = + Put.newBuilder() + .namespace(SOME_DEFAULT_NAMESPACE) + .table(NAMESPACE_TABLE_NAME) + .partitionKey(Key.ofText(NAMESPACE_COLUMN_NAME, SOME_NAMESPACE)) + .condition(ConditionBuilder.putIfNotExists()) + .build(); + when(config.getNamespace()).thenReturn(SOME_DEFAULT_NAMESPACE); + // This is an IllegalArgumentException but with a different error code (not retryable) + IllegalArgumentException toThrow = + new IllegalArgumentException("Some other error that should not trigger retry"); + doThrow(toThrow).when(storage).put(expectedPut); + + // Act Assert + assertThatThrownBy(() -> namespaceRegistry.create(SOME_NAMESPACE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Some other error that should not trigger retry"); + // Should only attempt once (no retry) + verify(storageAdmin, times(1)) + .createTable(SOME_DEFAULT_NAMESPACE, NAMESPACE_TABLE_NAME, NAMESPACE_TABLE_METADATA, true); + verify(storageAdmin, times(1)).createNamespace(fullNamespace, true); + verify(transactionAdmin, times(1)) + .createTable(fullNamespace, SOME_TABLE_NAME_1, tableMetadata1, true); + verify(transactionAdmin, times(1)) + .createTable(fullNamespace, SOME_TABLE_NAME_2, tableMetadata2, true); + verify(storageAdmin, times(1)) + .createTable(fullNamespace, SOME_TABLE_NAME_3, tableMetadata3, true); + verify(storageAdmin, times(1)) + .createTable(fullNamespace, SOME_TABLE_NAME_4, tableMetadata4, true); + verify(storage, times(1)).put(expectedPut); + } } From 7aab7fbfe5393798572ffe5dccefccef34f2d6a8 Mon Sep 17 00:00:00 2001 From: Jun Nemoto Date: Wed, 3 Dec 2025 15:57:11 +0900 Subject: [PATCH 5/5] Remove unnecessary sleep --- .../database/scalardb/AbstractScalarNamespaceRegistry.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java index 148a28e2..e820fdb8 100644 --- a/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java +++ b/ledger/src/main/java/com/scalar/dl/ledger/database/scalardb/AbstractScalarNamespaceRegistry.java @@ -4,7 +4,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.Uninterruptibles; import com.scalar.db.api.ConditionBuilder; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; @@ -26,7 +25,6 @@ import io.github.resilience4j.retry.RetryConfig; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.slf4j.Logger; @@ -121,8 +119,6 @@ private void createNamespace(String namespace) { try { String fullNamespaceName = config.getNamespace() + NAMESPACE_NAME_SEPARATOR + namespace; storageAdmin.createNamespace(fullNamespaceName, true); - // this sleep can be removed after negative cache is disabled in ScalarDB - Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); createStorageTables(fullNamespaceName); createTransactionTables(fullNamespaceName); } catch (ExecutionException e) {