Goten Server Library
The server should more or less be already known from the developer guide. We will provide some missing bits here only.
When we talk about servers, we can distinguish:
- gRPC Server instance that is listening on a TCP port.
- Server handler sets that implement some Service GRPC interface.
To underline what I mean, look at the following code snippet from IAM:
grpcServer := grpcserver.NewGrpcServer(
authenticator.AuthFunc(),
commonCfg.GetGrpcServer(),
log,
)
v1LimMixinServer := v1limmixinserver.NewLimitsMixinServer(
commonCfg,
limMixinStore,
authInfoProvider,
envRegistry,
policyStore,
)
v1alpha2LimMixinServer := v1alpha2limmixinserver.NewTransformedLimitsMixinServer(
v1LimMixinServer,
)
schemaServer := v1schemaserver.NewSchemaMixinServer(
commonCfg,
schemaStore,
v1Store,
policyStore,
authInfoProvider,
v1client.GetIAMDescriptor(),
)
v1alpha2MetaMixinServer := metamixinserver.NewMetaMixinTransformerServer(
schemaServer,
envRegistry,
)
v1Server := v1server.NewIAMServer(
ctx,
cfg,
v1Store,
authenticator,
authInfoProvider,
envRegistry,
policyStore,
)
v1alpha2Server := v1alpha2server.NewTransformedIAMServer(
cfg,
v1Server,
v1Store,
authInfoProvider,
)
v1alpha2server.RegisterServer(
grpcServer.GetHandle(),
v1alpha2Server,
)
v1server.RegisterServer(grpcServer.GetHandle(), v1Server)
metamixinserver.RegisterServer(
grpcServer.GetHandle(),
v1alpha2MetaMixinServer,
)
v1alpha2limmixinserver.RegisterServer(
grpcServer.GetHandle(),
v1alpha2LimMixinServer,
)
v1limmixinserver.RegisterServer(
grpcServer.GetHandle(),
v1LimMixinServer,
)
v1schemaserver.RegisterServer(
grpcServer.GetHandle(),
schemaServer,
)
v1alpha2diagserver.RegisterServer(
grpcServer.GetHandle(),
v1alpha2diagserver.NewDiagnosticsMixinServer(),
)
v1diagserver.RegisterServer(
grpcServer.GetHandle(),
v1diagserver.NewDiagnosticsMixinServer(),
)
There, an instance called grpcServer
is an actual GRPC Server instance
listening on a TCP port. If you dive into this implementation, you should
notice we are constructing an EdgelqGrpcServer
structure. It may consist
of actually two port listening instances:
googleGrpcServer *grpc.Server
, which is initialized with a set of unary and stream interceptors, optional TLS.websocketHTTPServer *http.Server
, which is initialized only if the websocket port was set. It delegates handling to improbableGrpcwebServer, which uses googleGrpcServer.
This Google server is the primary one and handles regular gRPC calls. The reason for the additional HTTP server is that we need to support web browsers, which cannot support native gRPC protocol. Instead:
- grpcweb is needed to handle unary and server-streaming calls.
- websockets are needed for bidirectional streaming calls.
Additionally, we have REST API support…
We have this envoy proxy sidecar, a separate container running next to the server instance. It handles all REST API, converting to native gRPC. It converts grpcweb into native grpc too, but has issues with websockets. For this reason, we added a Golang HTTP server with an improbable gRPC web instance. This improbable grpc web instance can handle both grpcweb and websockets, but we use it for websockets only, since it is missing from envoy proxy.
In theory, an improbable web server would be able to handle ALL protocols, but there is a drawback: For native gRPC calls will be less performant than the native grpc server (and ServeHTTP is less maintained). It is recommended to keep them separate, so we will stick with 2 ports. We may have some opportunity to remove the envoy proxy though.
Returning to the googleGrpcServer instance, we have all stream/unary
interceptors that are common for all calls, but this does not implement
the actual interface we expect from gRPC servers. Each service version
provides a complete interface to implement. For example, see the IAMServer
interface in this file:
https://github.com/cloudwan/edgelq/blob/main/iam/server/v1/iam/iam.pb.grpc.go.
Those server interfaces are in files ending with pb.grpc.go
.
To have a full server, we need to combine the GRPC Server instance for
SPEKTRA Edge (EdgelqGrpcServer
), with, let’s make up some name for it:
A business logic server instance (set of handlers). In this iam.pb.grpc.go
file this business logic instance is iamServer
. Going back to the main.go
snippet that is provided way above, we are registering eight business
logic servers (handler sets) on the provided *grpc.Server
instance.
As long as paths are unique across all, it is fine to register as many as
we can. Typically, we must include primary service for all versions, then
all mixins in all versions.
Those business logic servers provide code-generated middleware, typically executed in this order:
- Multi-region routing middleware (may redirect processing somewhere else, or split across many regions).
- Authorization middleware (may use a local cache, or send a request to IAM to obtain fresh role bindings).
- Transaction middleware (configures access to the database, for snapshot transactions and establishes new session).
- Outer middleware, which provides validation, and common outer operations for certain CRUD requests. For example, for update calls, it will ensure the resource exists and apply an update mask to achieve the final resource to save.
- Optional custom middleware and server code - which are responsible for final execution.
Transaction middleware also may repeat execution of all internal middleware
- core server, if the transaction needs to be repeated.
There are also “initial handlers” in generated pb.grpc.go
files. For
example, see this file:
https://github.com/cloudwan/edgelq/blob/main/iam/server/v1/group/group_service.pb.grpc.go.
For example, you can see _GroupService_GetGroup_Handler
as example for
unary, and _GroupService_WatchGroup_Handler
as an example for streaming
calls.
It is worth mentioning how interceptors play with middleware and these
“initial handlers”. Let’s copy and paste interceptors from the current
edgelq/common/serverenv/grpc/server.go
file:
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_ctxtags.StreamServerInterceptor(),
grpc_logrus.StreamServerInterceptor(
log,
grpc_logrus.WithLevels(codeToLevel),
),
grpc_recovery.StreamServerInterceptor(
grpc_recovery.WithRecoveryHandlerContext(recoveryHandler),
),
RespHeadersStreamServerInterceptor(),
grpc_auth.StreamServerInterceptor(authFunc),
PayloadStreamServerInterceptor(log, PayloadLoggingDecider),
grpc_validator.StreamServerInterceptor(),
)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_ctxtags.UnaryServerInterceptor(),
grpc_logrus.UnaryServerInterceptor(
log,
grpc_logrus.WithLevels(codeToLevel),
),
grpc_recovery.UnaryServerInterceptor(
grpc_recovery.WithRecoveryHandlerContext(recoveryHandler),
),
RespHeadersUnaryServerInterceptor(),
grpc_auth.UnaryServerInterceptor(authFunc),
PayloadUnaryServerInterceptor(log, PayloadLoggingDecider),
grpc_validator.UnaryServerInterceptor(),
)),
Unary requests are executed in the following way:
- Function
_GroupService_GetGroup_Handler
is called first! It calls the first interceptor but before that, it creates a handler that wraps the first middleware and passes to the interceptor chain. - The first interceptor is:
grpc_ctxtags.UnaryServerInterceptor()
. It calls the handler passed, which is the next interceptor. - The next interceptor is
grpc_logrus.UnaryServerInterceptor
and so on. At some point, we are calling the interceptor executing authentication. - The last interceptor (
grpc_validator.UnaryServerInterceptor()
) calls finally handler created byGroupService_GetGroup_Handler
. - First middleware is called. The call is executed through the middleware chain, and may reach the core server, but may return earlier.
- Interceptors are unwrapping in reverse order.
It is visible how this is called from the ChainUnaryServer
implementation
if you look.
Streaming calls are a bit different because we start from the interceptors themselves:
- gRPC Server instance takes function
_GroupService_WatchGroup_Handler
and casts intogrpc.StreamHandler
type. - Object
grpc.StreamHandler
, which is a handler for our method, is passed to the interceptor chain. During the chaining process,grpc.StreamHandler
is wrapped with all streaming interceptors, starting from the last. Therefore, the most internal StreamHandler will be_GroupService_WatchGroup_Handler
. grpc_ctxtags.StreamServerInterceptor()
is the entry point! It then invokes the next interceptors, and we go further and further, till we reach_GroupService_WatchGroup_Handler
, which is called by the last stream interceptor,grpc_validator.StreamServerInterceptor()
.- Middlewares are executed in the same way as always.
See the ChainStreamServer
implementation if you don’t believe it.
In total, this should give an idea of how the server works and what are the layers.