mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
237 lines
6.0 KiB
Go
237 lines
6.0 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/formancehq/stack/components/stargate/internal/api"
|
|
"github.com/formancehq/stack/components/stargate/internal/server/grpc/metrics"
|
|
"github.com/formancehq/stack/components/stargate/internal/utils"
|
|
"github.com/formancehq/stack/libs/go-libs/logging"
|
|
"github.com/google/uuid"
|
|
"github.com/nats-io/nats.go"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/metric"
|
|
"golang.org/x/sync/errgroup"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type Server struct {
|
|
api.UnimplementedStargateServiceServer
|
|
|
|
logger logging.Logger
|
|
natsConn *nats.Conn
|
|
metricsRegistry metrics.MetricsRegistry
|
|
}
|
|
|
|
func NewServer(
|
|
logger logging.Logger,
|
|
natsConn *nats.Conn,
|
|
metricsRegistry metrics.MetricsRegistry,
|
|
) *Server {
|
|
return &Server{
|
|
logger: logger,
|
|
natsConn: natsConn,
|
|
metricsRegistry: metricsRegistry,
|
|
}
|
|
}
|
|
|
|
type waitingResponse struct {
|
|
msg *nats.Msg
|
|
sendAt time.Time
|
|
}
|
|
|
|
type waitingPingResponse struct {
|
|
resp chan struct{}
|
|
}
|
|
|
|
func (s *Server) Stargate(stream api.StargateService_StargateServer) error {
|
|
ctx := stream.Context()
|
|
organizationID, stackID, err := orgaAndStackIDFromIncomingContext(ctx)
|
|
if err != nil {
|
|
s.metricsRegistry.StreamErrors().Add(ctx, 1, metric.WithAttributes([]attribute.KeyValue{
|
|
attribute.Int("code", int(codes.InvalidArgument)),
|
|
}...))
|
|
return status.Errorf(codes.InvalidArgument, "cannot get organization and stack id from contex metadata: %v", err)
|
|
}
|
|
|
|
logger := s.logger.WithFields(map[string]any{
|
|
"organization_id": organizationID,
|
|
"stack_id": stackID,
|
|
})
|
|
metrics.ClientsConnected.Add(1)
|
|
defer metrics.ClientsConnected.Add(-1)
|
|
|
|
logger.Infof("[GRPC] new stargate connection")
|
|
defer logger.Infof("[GRPC] stargate connection closed")
|
|
|
|
waitingResponses := sync.Map{}
|
|
waitingPingResponses := sync.Map{}
|
|
|
|
subject := utils.GetNatsSubject(organizationID, stackID)
|
|
logger.Debugf("[GRPC] subscribing to nats subject %s", subject)
|
|
sub, err := s.natsConn.QueueSubscribeSync(subject, subject)
|
|
if err != nil {
|
|
s.metricsRegistry.StreamErrors().Add(ctx, 1, metric.WithAttributes([]attribute.KeyValue{
|
|
attribute.Int("code", int(codes.Internal)),
|
|
}...))
|
|
return status.Errorf(codes.Internal, "cannot subscribe to nats subject")
|
|
}
|
|
defer sub.Unsubscribe()
|
|
|
|
eg, ctx := errgroup.WithContext(ctx)
|
|
|
|
eg.Go(func() error {
|
|
for {
|
|
msg, err := sub.NextMsgWithContext(ctx)
|
|
if err != nil {
|
|
if err == context.Canceled {
|
|
return nil
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var request api.StargateServerMessage
|
|
if err := proto.Unmarshal(msg.Data, &request); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch ev := request.Event.(type) {
|
|
case *api.StargateServerMessage_ApiCall:
|
|
logger.WithFields(map[string]any{
|
|
"path": ev.ApiCall.Path,
|
|
}).Debug("[GRPC] stream api call request received")
|
|
default:
|
|
}
|
|
|
|
correlationID := uuid.New().String()
|
|
|
|
waitingResponses.Store(correlationID, waitingResponse{
|
|
msg: msg,
|
|
sendAt: time.Now(),
|
|
})
|
|
request.CorrelationId = correlationID
|
|
|
|
if err := stream.Send(&request); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
})
|
|
|
|
eg.Go(func() error {
|
|
for {
|
|
in, err := stream.Recv()
|
|
if err != nil {
|
|
if err == io.EOF || status.Code(err) == codes.Canceled {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
switch in.Event.(type) {
|
|
case *api.StargateClientMessage_ApiCallResponse:
|
|
logger.Debugf("[GRPC] stream api call response received")
|
|
|
|
entry, ok := waitingResponses.LoadAndDelete(in.CorrelationId)
|
|
if !ok {
|
|
s.metricsRegistry.CorrelationIDNotFound().Add(ctx, 1)
|
|
continue
|
|
}
|
|
wr := entry.(waitingResponse)
|
|
|
|
s.metricsRegistry.GRPCLatencies().Record(ctx, time.Since(wr.sendAt).Milliseconds(), metric.WithAttributes([]attribute.KeyValue{
|
|
attribute.String("name", fmt.Sprintf("%s.%s", organizationID, stackID)),
|
|
}...))
|
|
|
|
data, err := proto.Marshal(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := wr.msg.Respond(data); err != nil {
|
|
return err
|
|
}
|
|
case *api.StargateClientMessage_Pong_:
|
|
entry, ok := waitingPingResponses.LoadAndDelete(in.CorrelationId)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
wpr := entry.(waitingPingResponse)
|
|
close(wpr.resp)
|
|
}
|
|
}
|
|
})
|
|
|
|
// We have to implement a ping/pong system to detect dead connections.
|
|
// We cannot use grpc keepalive to do that because AWS ALB does not support
|
|
// raw HTTP/2 frames.
|
|
// c.f.: https://stackoverflow.com/questions/66818645/http2-ping-frames-over-aws-alb-grpc-keepalive-ping
|
|
eg.Go(func() error {
|
|
for {
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
correlationID := uuid.New().String()
|
|
resp := make(chan struct{})
|
|
waitingPingResponses.Store(correlationID, waitingPingResponse{
|
|
resp: resp,
|
|
})
|
|
|
|
if err := stream.Send(&api.StargateServerMessage{
|
|
CorrelationId: correlationID,
|
|
Event: &api.StargateServerMessage_Ping_{Ping: &api.StargateServerMessage_Ping{}},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
logger.Debugf("[GRPC] ping timeout")
|
|
return status.Errorf(codes.DeadlineExceeded, "ping timeout")
|
|
case <-resp:
|
|
// Pong received, do nothing
|
|
}
|
|
case <-ctx.Done():
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
s.metricsRegistry.StreamErrors().Add(ctx, 1, metric.WithAttributes([]attribute.KeyValue{
|
|
attribute.Int("code", int(codes.Internal)),
|
|
}...))
|
|
return status.Errorf(codes.Internal, "internal error: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func orgaAndStackIDFromIncomingContext(ctx context.Context) (string, string, error) {
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
if !ok {
|
|
return "", "", errors.New("no metadata from incoming context")
|
|
}
|
|
|
|
organizationID := md.Get("organization-id")
|
|
if len(organizationID) == 0 {
|
|
return "", "", errors.New("no organization-id in metadata")
|
|
}
|
|
|
|
stackID := md.Get("stack-id")
|
|
if len(stackID) == 0 {
|
|
return "", "", errors.New("no stack-id in metadata")
|
|
}
|
|
|
|
return organizationID[0], stackID[0], nil
|
|
}
|