mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-24 05:18:58 +00:00
210 lines
6.9 KiB
Markdown
210 lines
6.9 KiB
Markdown
# Service
|
|
|
|
Contains the base classes for the services. This is where port configuration,
|
|
and common endpoints are set up. The service base class is additionally responsible for
|
|
registering the service with the service registry, and announcing its liveness.
|
|
|
|
## Creating a new Service
|
|
|
|
The minimal service needs a `MainClass` and a `Service` class.
|
|
|
|
For proper initiation, the main class should look like the below. It will
|
|
create a Guice injector, and then create an instance of the service, and take
|
|
care of loading configuration properties and setting up loggers in a sane
|
|
way.
|
|
|
|
```java
|
|
public class FoobarMain extends MainClass {
|
|
|
|
@Inject
|
|
public FoobarMain(FoobarService service) {}
|
|
|
|
public static void main(String... args) {
|
|
init(ServiceId.Foobar, args);
|
|
|
|
Injector injector = Guice.createInjector(
|
|
new FoobarModule(), /* optional custom bindings go here */
|
|
new DatabaseModule(),
|
|
new ConfigurationModule(ServiceId.Foobar));
|
|
|
|
injector.getInstance(FoobarMain.class);
|
|
|
|
// set the service as ready so that delayed tasks can be started
|
|
injector.getInstance(Initialization.class).setReady();
|
|
}
|
|
}
|
|
```
|
|
|
|
A service class has a boilerplate set-up that looks like this:
|
|
|
|
```java
|
|
@Singleton
|
|
public class FoobarService extends Service {
|
|
|
|
@Inject
|
|
public FoobarService(BaseServiceParams params) {
|
|
super(params, List.of(/* grpc services */));
|
|
|
|
// set up any Spark endpoints here
|
|
}
|
|
}
|
|
```
|
|
|
|
The service should also be given a canonical name in the `ServiceId` enum.
|
|
|
|
## Central Classes
|
|
|
|
* [MainClass](java/nu/marginalia/service/MainClass.java) bootstraps all executables
|
|
* [Service](java/nu/marginalia/service/server/Service.java) base class for all services.
|
|
|
|
---
|
|
|
|
# Service Discovery
|
|
|
|
The module also contains classes for helping services discover each other,
|
|
and managing connections between them.
|
|
|
|
## Service Registry
|
|
|
|
The service registry is a class that keeps track of the services
|
|
that are currently running, and their connection information.
|
|
|
|
The service register implementation is based on [Zookeeper](https://zookeeper.apache.org/),
|
|
which is a distributed coordination service. This lets services register
|
|
themselves and announce their liveness, and then discover each other.
|
|
|
|
It supports multiple instances of a service running, and
|
|
supports running the system bare-metal, where it will assign
|
|
ports to the services from a range.
|
|
|
|
* REST services are registered on a per-node basis, and are always non-partitioned.
|
|
* gRPC services are registered on a per-api basis, and can be partitioned
|
|
or non-partitioned. This means that if a gRPC api is moved between nodes,
|
|
the clients will not need to be reconfigured.
|
|
|
|
To be discoverable, the caller must first register their
|
|
services:
|
|
|
|
```java
|
|
// Register one or more services
|
|
serviceRegistry.registerService(
|
|
ServiceKey.forRest(serviceId, nodeId),
|
|
instanceUuid, // unique
|
|
externalAddress); // bind-address
|
|
|
|
// Non-partitioned GRPC service
|
|
serviceRegistry.registerService(
|
|
ServiceKey.forServiceDescriptor(descriptor, ServicePartition.any()),
|
|
instanceUuid,
|
|
externalAddress);
|
|
|
|
// Partitioned GRPC service
|
|
serviceRegistry.registerService(
|
|
ServiceKey.forServiceDescriptor(descriptor, ServicePartition.partition(5)),
|
|
instanceUuid,
|
|
externalAddress);
|
|
|
|
// (+ any other services)
|
|
```
|
|
|
|
Then, the caller must announce their instance. Before this is done,
|
|
the service is not discoverable.
|
|
|
|
```java
|
|
registry.announceInstance(instanceUUID);
|
|
```
|
|
|
|
All of this is done automatically by the `Service` base class
|
|
in the [service](../service/) module.
|
|
|
|
To discover a service, the caller can query the registry:
|
|
|
|
```java
|
|
Set<InstanceAddress> endpoints = registry.getEndpoints(serviceKey);
|
|
```
|
|
|
|
It's also possible to subscribe to changes in the registry, so that
|
|
the caller can be notified when a service comes or goes, with `registry.registerMonitor()`.
|
|
|
|
However the `GrpcChannelPoolFactory` is a more convenient way to access the services,
|
|
it will let the caller create a pool of channels to the services, and manage their
|
|
lifecycle, listen to lifecycle notifications and so on.
|
|
|
|
## gRPC Channel Pool
|
|
|
|
From the [GrpcChannelPoolFactory](java/nu/marginalia/service/client/GrpcChannelPoolFactory.java), two types of channel pools can be created
|
|
that are aware of the service registry:
|
|
|
|
* [GrpcMultiNodeChannelPool](java/nu/marginalia/service/client/GrpcMultiNodeChannelPool.java) - This pool permits 1-n style communication with partitioned services
|
|
* [GrpcSingleNodeChannelPool](java/nu/marginalia/service/client/GrpcSingleNodeChannelPool.java) - This pool permits 1-1 style communication with non-partitioned services.
|
|
if multiple instances are running, it will use one of them and fall back
|
|
to another if the first is not available.
|
|
|
|
The pools can generate calls to the gRPC services, and will manage the lifecycle of the channels.
|
|
|
|
The API is designed to be simple to use, and will permit the caller to access the Stub interfaces
|
|
for the services through a fluent API.
|
|
|
|
### Example Usage of the GrpcSingleNodeChannelPool
|
|
|
|
```java
|
|
// create a pool for a non-partitioned service
|
|
channelPool = factory.createSingle(
|
|
ServiceKey.forGrpcApi(MathApiGrpc.class, ServicePartition.any()),
|
|
MathApiGrpc::newBlockingStub);
|
|
|
|
// blocking call
|
|
Response response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.run(request);
|
|
|
|
// sequential blocking calls
|
|
List<Response> response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.runFor(request1, request2);
|
|
|
|
|
|
// async call
|
|
Future<Response> response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.async(myExecutor)
|
|
.run(request);
|
|
|
|
// multiple async calls
|
|
Future<List<Response>> response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.async(myExecutor)
|
|
.runFor(request1, request2);
|
|
```
|
|
|
|
### Example Usage of the GrpcSingleNodeChannelPool
|
|
|
|
```java
|
|
// create a pool for a partitioned service
|
|
channelPool = factory.createMulti(
|
|
ServiceKey.forGrpcApi(MathApiGrpc.class, ServicePartition.multi()),
|
|
MathApiGrpc::newBlockingStub);
|
|
|
|
// blocking call
|
|
List<Response> response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.run(request);
|
|
|
|
// async call
|
|
Future<List<Response>> response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.async(myExecutor)
|
|
.runEach(request);
|
|
|
|
// async call, will fail or succeed as a group
|
|
Future<List<Response>> response = channelPool
|
|
.call(MathApiGrpc.MathApiBlockingStub::dictionaryLookup)
|
|
.async(myExecutor)
|
|
.runAll(request1, request2);
|
|
```
|
|
|
|
|
|
### Central Classes
|
|
|
|
* [ServiceRegistryIf](java/nu/marginalia/service/discovery/ServiceRegistryIf.java)
|
|
* [ZkServiceRegistry](java/nu/marginalia/service/discovery/ZkServiceRegistry.java) |