Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECR-4133] Add service resume #1372

Merged
merged 9 commits into from
Jan 23, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion exonum-java-binding/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `supervisor-mode` CLI parameter added for `generate-template` command. It
allows to configure the mode of the Supervisor service. Possible values are
"simple" and "decentralized". (#1361)
- Service instances can be stopped now. (#1358)
- Support of service instances lifecycle: they can be activated, stopped and resumed now.
Also, service instance artifacts can be upgraded before resuming (but with no data changes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No data changes, according to the recent discussions, is no longer correct.

I'd add to the list below 'synchronous data migration'.

which allows services API update, add new service transactions etc. (#1358, #1372)

[blockchain-proofs]: https://exonum.com/doc/api/java-binding/0.10.0-SNAPSHOT/com/exonum/binding/core/blockchain/Blockchain.html#proofs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.exonum.binding.core.runtime;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.exonum.binding.common.crypto.PublicKey;
Expand All @@ -34,7 +35,7 @@ class MultiplexingNodeDecorator implements Node {
private boolean closed;

MultiplexingNodeDecorator(Node node) {
this.node = node;
this.node = checkNotNull(node);
this.closed = false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public boolean isArtifactDeployed(ServiceArtifactId id) {
* @throws ExecutionException if such exception occurred in the service constructor;
* must be translated into an error of kind {@link ErrorKind#SERVICE}
* @throws UnexpectedExecutionException if any other exception occurred in
* the the service constructor; it is included as cause. The cause must be translated
* the service constructor; it is included as cause. The cause must be translated
* into an error of kind {@link ErrorKind#UNEXPECTED}
* @throws RuntimeException if the runtime failed to instantiate the service for other reason
*/
Expand All @@ -206,6 +206,39 @@ public void initiateAddingService(Fork fork, ServiceInstanceSpec instanceSpec,
}
}

/**
* Initiates resuming of previously stopped service instance. Service instance artifact could
* be upgraded in advance to bring some new functionality.
*
* @param fork a database view to apply configuration
* @param instanceSpec a service instance specification; must reference a deployed artifact
* @param configuration a service instance configuration parameters as a serialized protobuf
* message
* @throws IllegalArgumentException if the given service instance is active; or its artifact
* is not deployed
* @throws ExecutionException if such exception occurred in the service method;
* must be translated into an error of kind {@link ErrorKind#SERVICE}
* @throws UnexpectedExecutionException if any other exception occurred in
* the service method; it is included as cause. The cause must be translated
* into an error of kind {@link ErrorKind#UNEXPECTED}
* @throws RuntimeException if the runtime failed to resume the service for other reason
*/
public void initializeResumingService(Fork fork, ServiceInstanceSpec instanceSpec,
byte[] configuration) {
try {
synchronized (lock) {
checkStoppedService(instanceSpec.getId());
ServiceWrapper service = createServiceInstance(instanceSpec);
service.resume(fork, new ServiceConfiguration(configuration));
}
logger.info("Resumed service: {}", instanceSpec);
} catch (Exception e) {
logger.error("Failed to resume a service {} instance with parameters {}",
instanceSpec, configuration, e);
throw e;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this class is responsible to log errors, though there is ECR-3813 to shift that.

}

/**
* Modifies the state of the given service instance at the runtime either by activation it or
* stopping. The service instance should be successfully initialized
Expand Down Expand Up @@ -486,16 +519,24 @@ public void close() throws InterruptedException {
}

private ServiceWrapper getServiceById(Integer serviceId) {
checkService(serviceId);
checkActiveService(serviceId);
return servicesById.get(serviceId);
}

/** Checks that the service with the given id is started in this runtime. */
private void checkService(Integer serviceId) {
private void checkActiveService(Integer serviceId) {
checkArgument(servicesById.containsKey(serviceId),
"No service with id=%s in the Java runtime", serviceId);
}

/** Checks that the service with the given id is not active in this runtime. */
private void checkStoppedService(Integer serviceId) {
ServiceWrapper activeService = servicesById.get(serviceId);
checkArgument(activeService == null,
"Service with id=%s should be stopped, but actually active. "
+ "Found active service instance: %s", serviceId, activeService);
}

@VisibleForTesting
Optional<ServiceWrapper> findService(String name) {
return Optional.ofNullable(services.get(name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,27 @@ void initiateAddingService(long forkHandle, byte[] instanceSpec, byte[] configur
}
}

/**
* Starts resuming a service with the given specification.
*
* @param forkHandle a handle to a native fork object
* @param instanceSpec the service instance specification as a serialized {@link InstanceSpec}
* protobuf message
* @param configuration the service initial configuration parameters as a serialized protobuf
* message
* @see ServiceRuntime#initializeResumingService(Fork, ServiceInstanceSpec, byte[])
*/
void initializeResumingService(long forkHandle, byte[] instanceSpec, byte[] configuration)
throws CloseFailuresException {
try (Cleaner cleaner = new Cleaner()) {
Fork fork = viewFactory.createFork(forkHandle, cleaner);
ServiceInstanceSpec javaInstanceSpec = parseInstanceSpec(instanceSpec);
serviceRuntime.initializeResumingService(fork, javaInstanceSpec, configuration);
} catch (CloseFailuresException e) {
handleCloseFailure(e);
}
}

/**
* Updates the status of the service instance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ void initialize(Fork view, Configuration configuration) {
callServiceMethod(() -> service.initialize(view, configuration));
}

void resume(Fork view, Configuration configuration) {
callServiceMethod(() -> service.resume(view, configuration));
}

void executeTransaction(String interfaceName, int txId, byte[] arguments, int callerServiceId,
TransactionContext context) {
switch (interfaceName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ default void initialize(Fork fork, Configuration configuration) {
// No configuration
}

/**
* Resumes the previously stopped service instance. This method is called when
* a stopped service instance is restarted.
*
* <p>This method may perform any changes to the database. For example, update some service
* parameters, deprecate old entries etc.
*
* <p>Also, note that performing any bulk operations or data migration
* <em>is not recommended</em> here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add short motivation, as Ostrovski clarified in the discussion.

* <!--TODO: Add a link to the migration procedure -->
*
* @param fork a database fork to apply changes to. Not valid after this method returns
* @param configuration the service configuration parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we've discussed, those are hardly configuration parameters, just generic 'arguments'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are 2 questions here:

  • should we use Configuration type or byte[]? Configuration looks more convenient
  • If we use Configuration class for the resume operation then it's javadoc and, probably, name (ServiceArgumests, ServiceParameters?) should be updated.

Also, talking about Configuration, what is the reason for having interface + implementation(ServiceConfiguration), not just VO class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

byte[] since is it is not configuration. We shall not use Configuration for this operation — it is for configuration parameters.

It could be made one, it is made in this way probably to give more flexibility in terms of implementation (e.g., if we went with also properties).

* @throws ExecutionException if the configuration parameters are not valid (e.g.,
* malformed, or do not meet the preconditions)
*/
default void resume(Fork fork, Configuration configuration) {
// No configuration
}

/**
* Creates handlers that make up the public HTTP API of this service.
* The handlers are added to the given router, which is then mounted at the following path:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,33 @@ void addService() throws CloseFailuresException {
verify(serviceRuntime).initiateAddingService(fork, expected, configuration);
}

@Test
void initializeResumingService() throws CloseFailuresException {
long forkHandle = 0x110b;
Cleaner cleaner = new Cleaner();
Fork fork = Fork.newInstance(forkHandle, false, cleaner);
when(viewFactory.createFork(eq(forkHandle), any(Cleaner.class)))
.thenReturn(fork);

int serviceId = 1;
String serviceName = "s1";
ArtifactId artifact = ARTIFACT_ID;
byte[] instanceSpec = InstanceSpec.newBuilder()
.setId(serviceId)
.setName(serviceName)
.setArtifact(artifact)
.build()
.toByteArray();
byte[] configuration = bytes(1, 2);

serviceRuntimeAdapter.initializeResumingService(forkHandle, instanceSpec, configuration);

// Check the runtime was invoked with correct config
ServiceInstanceSpec expected = ServiceInstanceSpec.newInstance(serviceName, serviceId,
ServiceArtifactId.fromProto(artifact));
verify(serviceRuntime).initializeResumingService(fork, expected, configuration);
}

@Test
void afterTransactions() throws CloseFailuresException {
int serviceId = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,6 @@ void activateServiceDuplicate() {

@Test
void stopNonActiveService() {
serviceRuntime.initialize(mock(Node.class));

ServiceArtifactId artifactId = ServiceArtifactId.newJavaId("com.acme/foo-service", "1.0.0");
ServiceInstanceSpec instanceSpec = ServiceInstanceSpec.newInstance(TEST_NAME,
TEST_ID, artifactId);
Expand Down Expand Up @@ -369,6 +367,68 @@ void updateServiceStatusBadStatus(Status badStatus) {
assertThat(exception).hasMessageContaining(instanceSpec.getName());
}

@Test
void initializeResumingService() {
serviceRuntime.initialize(mock(Node.class));

ServiceArtifactId artifactId = ServiceArtifactId.parseFrom("1:com.acme/foo-service:1.0.0");
LoadedServiceDefinition serviceDefinition = LoadedServiceDefinition
.newInstance(artifactId, TestServiceModule::new);
ServiceInstanceSpec instanceSpec = ServiceInstanceSpec.newInstance(TEST_NAME,
TEST_ID, artifactId);
when(serviceLoader.findService(artifactId))
.thenReturn(Optional.of(serviceDefinition));

ServiceWrapper serviceWrapper = mock(ServiceWrapper.class);
when(servicesFactory.createService(eq(serviceDefinition), eq(instanceSpec),
any(MultiplexingNodeDecorator.class)))
.thenReturn(serviceWrapper);

// Create the service from the artifact
Fork fork = mock(Fork.class);
byte[] configuration = anyConfiguration();
serviceRuntime.initializeResumingService(fork, instanceSpec, configuration);

// Check it was instantiated as expected
verify(servicesFactory).createService(eq(serviceDefinition), eq(instanceSpec),
any(MultiplexingNodeDecorator.class));

// and the service was configured
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resumed

Configuration expectedConfig = new ServiceConfiguration(configuration);
verify(serviceWrapper).resume(fork, expectedConfig);

// but not registered in the runtime yet:
assertThat(serviceRuntime.findService(TEST_NAME)).isEmpty();
}

@Test
void initializeResumingActiveService() {
serviceRuntime.initialize(mock(Node.class));

ServiceArtifactId artifactId = ServiceArtifactId.newJavaId("com.acme/foo-service", "1.0.0");
LoadedServiceDefinition serviceDefinition = LoadedServiceDefinition
.newInstance(artifactId, TestServiceModule::new);
ServiceInstanceSpec instanceSpec = ServiceInstanceSpec.newInstance(TEST_NAME,
TEST_ID, artifactId);
when(serviceLoader.findService(artifactId))
.thenReturn(Optional.of(serviceDefinition));

ServiceWrapper serviceWrapper = mock(ServiceWrapper.class);
when(serviceWrapper.getId()).thenReturn(TEST_ID);
when(serviceWrapper.getName()).thenReturn(TEST_NAME);
when(servicesFactory.createService(eq(serviceDefinition), eq(instanceSpec),
any(MultiplexingNodeDecorator.class)))
.thenReturn(serviceWrapper);

// Activate the service from the artifact
serviceRuntime.updateInstanceStatus(instanceSpec, Status.ACTIVE);

byte[] configuration = anyConfiguration();
Fork fork = mock(Fork.class);
assertThrows(IllegalArgumentException.class,
() -> serviceRuntime.initializeResumingService(fork, instanceSpec, configuration));
}

@Test
void shutdown() throws InterruptedException {
serviceRuntime.shutdown();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ void initialize() {
verify(service).initialize(fork, config);
}

@Test
void resume() {
Fork fork = mock(Fork.class);
Configuration config = new ServiceConfiguration(new byte[0]);
serviceWrapper.resume(fork, config);
verify(service).resume(fork, config);
}

@Test
void initializePropagatesExecutionException() {
ExecutionException e = new ExecutionException((byte) 1);
Expand Down