From d17c86d0ec076438ce04bc5ebd0230cdde1391bf Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Fri, 21 Aug 2026 08:54:03 +0200 Subject: [PATCH 1/6] [raft/scd] Extract update and create opintent --- pkg/scd/actions/operational_intents.go | 232 +++++++++++++++++++++++-- pkg/scd/operational_intents_handler.go | 219 +++-------------------- pkg/scd/server.go | 23 --- pkg/store/store.go | 11 +- 4 files changed, 255 insertions(+), 230 deletions(-) diff --git a/pkg/scd/actions/operational_intents.go b/pkg/scd/actions/operational_intents.go index 51f2162ed..ab94f5a34 100644 --- a/pkg/scd/actions/operational_intents.go +++ b/pkg/scd/actions/operational_intents.go @@ -14,6 +14,7 @@ import ( scdmodels "github.com/interuss/dss/pkg/scd/models" "github.com/interuss/dss/pkg/scd/repos" dssstore "github.com/interuss/dss/pkg/store" + "github.com/interuss/dss/pkg/timestamp" "github.com/interuss/stacktrace" ) @@ -35,6 +36,16 @@ func init() { Decode: dssstore.DecodeJSON[*restapi.DeleteOperationalIntentReferenceRequest], Execute: ExecuteDeleteOperationalIntentReference, } + Registry[restapi.CreateOperationalIntentReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ + Encode: dssstore.EncodeJSON, + Decode: dssstore.DecodeJSON[*restapi.CreateOperationalIntentReferenceRequest], + Execute: ExecutePutOperationalIntentReference, + } + Registry[restapi.UpdateOperationalIntentReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ + Encode: dssstore.EncodeJSON, + Decode: dssstore.DecodeJSON[*restapi.UpdateOperationalIntentReferenceRequest], + Execute: ExecutePutOperationalIntentReference, + } } // SubscriptionIsImplicitAndOnlyAttachedToOIR will check if: @@ -251,11 +262,11 @@ func CheckUpsertPermissionsAndReturnManager(authorizedManager *api.Authorization return dssmodels.Manager(*authorizedManager.ClientID), nil } -// ValidateUpsertRequestAgainstPreviousOIR checks that the client requesting an OIR upsert has the necessary permissions and that the request is valid. +// validateUpsertRequestAgainstPreviousOIR checks that the client requesting an OIR upsert has the necessary permissions and that the request is valid. // On success, the version of the OIR is returned: // - upon initial creation (if no previous OIR exists), it is 0 // - otherwise, it is the version of the previous OIR -func ValidateUpsertRequestAgainstPreviousOIR( +func validateUpsertRequestAgainstPreviousOIR( requestingManager dssmodels.Manager, providedOVN scdmodels.OVN, previousOIR *scdmodels.OperationalIntent, @@ -281,11 +292,11 @@ func ValidateUpsertRequestAgainstPreviousOIR( return nil } -// ComputeNotificationVolume computes the volume that needs to be queried for subscriptions +// computeNotificationVolume computes the volume that needs to be queried for subscriptions // given the requested extent and the (possibly nil) previous operational intent. // The returned volume is either the union of the requested extent and the previous OIR's extent, or just the requested extent // if the previous OIR is nil. -func ComputeNotificationVolume( +func computeNotificationVolume( previousOIR *scdmodels.OperationalIntent, requestedExtent *dssmodels.Volume4D) (*dssmodels.Volume4D, error) { @@ -330,7 +341,7 @@ type validOIRParams struct { Key map[scdmodels.OVN]bool } -func (vp *validOIRParams) ToOIR(manager dssmodels.Manager, attachedSub *scdmodels.Subscription, version scdmodels.VersionNumber, pastOVNs []scdmodels.OVN) *scdmodels.OperationalIntent { +func (vp *validOIRParams) toOIR(manager dssmodels.Manager, attachedSub *scdmodels.Subscription, version scdmodels.VersionNumber, pastOVNs []scdmodels.OVN) *scdmodels.OperationalIntent { // For OIR's in the accepted state, we may not have a attachedSub available, // in such cases the attachedSub ID on scdmodels.OperationalIntent will be nil // and will be replaced with the 'NullV4UUID' when sent over to a client. @@ -475,9 +486,9 @@ func ValidateAndReturnOIRUpsertParams( return valid, nil } -// CreateAndStoreNewImplicitSubscription will create a brand new implicit subscription based on the provided parameters, +// createAndStoreNewImplicitSubscription will create a brand new implicit subscription based on the provided parameters, // store it and return it. -func CreateAndStoreNewImplicitSubscription(ctx context.Context, r repos.Repository, manager dssmodels.Manager, validParams *validOIRParams) (*scdmodels.Subscription, error) { +func createAndStoreNewImplicitSubscription(ctx context.Context, r repos.Repository, manager dssmodels.Manager, validParams *validOIRParams) (*scdmodels.Subscription, error) { generator, err := random.Generator(random.MustFromContext(ctx), "implicit-subscription:"+validParams.ID.String()) if err != nil { return nil, stacktrace.Propagate(err, "Failed to derive implicit subscription ID generator") @@ -504,11 +515,11 @@ func CreateAndStoreNewImplicitSubscription(ctx context.Context, r repos.Reposito return r.UpsertSubscription(ctx, &subToUpsert) } -// ValidateKeyAndProvideConflictResponse ensures that the provided key contains all the necessary OVNs relevant for the area covered by the OperationalIntent. +// validateKeyAndProvideConflictResponse ensures that the provided key contains all the necessary OVNs relevant for the area covered by the OperationalIntent. // - If all required keys are provided, (nil, nil) will be returned. // - If keys are missing, the conflict response to be sent back as well as an error with the dsserr.MissingOVNs code will be returned. // - In case of any other error, (nil, error) will be returned. -func ValidateKeyAndProvideConflictResponse( +func validateKeyAndProvideConflictResponse( ctx context.Context, r repos.Repository, requestingManager dssmodels.Manager, @@ -584,10 +595,10 @@ func ValidateKeyAndProvideConflictResponse( return nil, nil } -// EnsureSubscriptionCoversOIR ensures that the subscription covers the requested geo-temporal extent, extending it if both possible and required, +// ensureSubscriptionCoversOIR ensures that the subscription covers the requested geo-temporal extent, extending it if both possible and required, // or failing otherwise. // After this method returns successfully, the subscription will cover the requested geo-temporal extent. -func EnsureSubscriptionCoversOIR(ctx context.Context, r repos.Repository, sub *scdmodels.Subscription, params *validOIRParams) (*scdmodels.Subscription, error) { +func ensureSubscriptionCoversOIR(ctx context.Context, r repos.Repository, sub *scdmodels.Subscription, params *validOIRParams) (*scdmodels.Subscription, error) { updateSub := false if sub.StartTime != nil && sub.StartTime.After(*params.UExtent.StartTime) { @@ -624,3 +635,202 @@ func EnsureSubscriptionCoversOIR(ctx context.Context, r repos.Repository, sub *s return sub, nil } + +// PutOperationalIntentReferenceResult is the result of an Operational Intent Reference put operation. +// Exactly one of Response or Conflict is set: Conflict is set when the upsert failed because of missing OVNs (a dsserr.MissingOVNs error is returned alongside it) +// and Response is set on success. +type PutOperationalIntentReferenceResult struct { + Response *restapi.ChangeOperationalIntentReferenceResponse + Conflict *restapi.AirspaceConflictResponse +} + +// ExecutePutOperationalIntentReference inserts or updates an Operational Intent. +// If the ovn argument is empty (""), it will attempt to create a new Operational Intent. +func ExecutePutOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { + var ( + entityid restapi.EntityID + ovn restapi.EntityOVN + params *restapi.PutOperationalIntentReferenceParameters + auth *api.AuthorizationResult + ) + + switch req := request.(type) { + case *restapi.CreateOperationalIntentReferenceRequest: + entityid, params, auth = req.Entityid, req.Body, &req.Auth + case *restapi.UpdateOperationalIntentReferenceRequest: + entityid, ovn, params, auth = req.Entityid, req.Ovn, req.Body, &req.Auth + default: + return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.CreateOperationalIntentReferenceOperationID) + } + + now := timestamp.MustGetRequestTimestamp(ctx) + + // Base URL scheme validation is a pre-flight, request-only check performed by the handler + // before this action is proposed for consensus; skip it here (allowHTTPBaseUrls: true). + validParams, err := ValidateAndReturnOIRUpsertParams(now, entityid, ovn, params, true) + if err != nil { + return nil, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters") + } + if auth.ClientID == nil { + return nil, stacktrace.NewErrorWithCode(dsserr.PermissionDenied, "Missing manager") + } + manager := dssmodels.Manager(*auth.ClientID) + + // Get existing OperationalIntent, if any + old, err := repo.GetOperationalIntent(ctx, validParams.ID) + if err != nil { + return nil, stacktrace.Propagate(err, "Could not get OperationalIntent from repo") + } + + // Lock subscriptions based on the cell and subscriptions we're going to use + // to reduce the number of retries under concurrent load. + // See issue #1002 for details. + var subscriptionIds = make([]dssmodels.ID, 0) + + if old != nil && old.SubscriptionID != nil { + subscriptionIds = append(subscriptionIds, *old.SubscriptionID) + } + + if !validParams.SubscriptionID.Empty() { + subscriptionIds = append(subscriptionIds, validParams.SubscriptionID) + } + + err = repo.LockSubscriptionsOnCells(ctx, validParams.Cells, subscriptionIds, validParams.UExtent.StartTime, validParams.UExtent.EndTime) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to acquire lock") + } + + // Validate the request against the previous OIR + if err := validateUpsertRequestAgainstPreviousOIR(manager, validParams.OVN, old); err != nil { + return nil, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "Request validation failed") + } + + var ( + version = scdmodels.VersionNumber(1) + pastOVNs = make([]scdmodels.OVN, 0) + previousSub *scdmodels.Subscription + ) + if old != nil { + version = old.Version + 1 + pastOVNs = append(old.PastOVNs, validParams.OVN) + + // Fetch the previous OIR's subscription if it exists + if old.SubscriptionID != nil { + previousSub, err = repo.GetSubscription(ctx, *old.SubscriptionID) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to get OperationalIntent's Subscription from repo") + } + } + } + + // Determine if the previous subscription is being replaced and if it will need to be cleaned up + previousSubIsBeingReplaced := previousSub != nil && validParams.SubscriptionID != previousSub.ID + removePreviousImplicitSubscription := false + if previousSubIsBeingReplaced { + removePreviousImplicitSubscription, err = SubscriptionIsImplicitAndOnlyAttachedToOIR(ctx, repo, validParams.ID, previousSub) + if err != nil { + return nil, stacktrace.Propagate(err, "Could not determine if previous Subscription can be removed") + } + } + + // attachedSub is the subscription that will end up being attached to the OIR + // it defaults to the previous subscription (which may be nil), and may be updated if required by the parameters + attachedSub := previousSub + if validParams.SubscriptionID.Empty() { + // No subscription ID was provided: + // check if an implicit subscription should be created, otherwise do nothing + if validParams.ImplicitSubscription.Requested { + // Parameters for a new implicit subscription have been passed: we will create + // a new implicit subscription even if another subscription was attached to this OIR before, + // regardless of whether it was an implicit subscription or not. + if attachedSub, err = createAndStoreNewImplicitSubscription(ctx, repo, manager, validParams); err != nil { + return nil, stacktrace.Propagate(err, "Failed to create implicit subscription") + } + } else { + // If no subscription ID is provided and no implicit subscription is requested, + // the OIR should have no attached subscription + attachedSub = nil + } + } else { + // Attempt to rely on the specified subscription + // If it is different from the previous subscription, we need to fetch it from the store + // in order to ensure it correctly covers the OIR. + // We do the check below in order to avoid re-fetching the subscription if it has not changed + if attachedSub == nil || previousSubIsBeingReplaced { + attachedSub, err = repo.GetSubscription(ctx, validParams.SubscriptionID) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to get requested Subscription from store") + } + if attachedSub == nil { + return nil, stacktrace.NewErrorWithCode(dsserr.BadRequest, "Specified Subscription %s does not exist", validParams.SubscriptionID) + } + } + + // We need to confirm that it is owned by the calling manager + if attachedSub.Manager != manager { + return nil, stacktrace.Propagate( + // We do a bit of wrapping gymnastics because the root error message will be sent in the response, + // and we don't want to include the effective manager in there. + stacktrace.NewErrorWithCode( + dsserr.PermissionDenied, "Specificed Subscription is owned by different client"), + // The propagation message will end in the logs and help with debugging. + "Subscription %s owned by %s, but %s attempted to use it for an OperationalIntent", + validParams.SubscriptionID, + attachedSub.Manager, + manager, + ) + } + + // We need to ensure the subscription covers the OIR's geo-temporal extent + attachedSub, err = ensureSubscriptionCoversOIR(ctx, repo, attachedSub, validParams) + if err != nil { + return nil, stacktrace.Propagate(err, "Failed to ensure subscription covers OIR") + } + } + + var responseConflict *restapi.AirspaceConflictResponse + if validParams.State.RequiresKey() { + responseConflict, err = validateKeyAndProvideConflictResponse(ctx, repo, manager, validParams, attachedSub) + if err != nil { + // responseConflict is non-nil here on a dsserr.MissingOVNs error: return it alongside + // the error so the handler can still send it to the client. See the doc comment above. + return &PutOperationalIntentReferenceResult{Conflict: responseConflict}, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "Failed to validate key") + } + } + + // Construct the new OperationalIntent + op := validParams.toOIR(manager, attachedSub, version, pastOVNs) + + // Upsert the OperationalIntent + op, err = repo.UpsertOperationalIntent(ctx, op) + if err != nil { + return nil, stacktrace.Propagate(err, "Failed to upsert OperationalIntent in repo") + } + + // Check if the previously attached subscription should be removed + if removePreviousImplicitSubscription { + err = repo.DeleteSubscription(ctx, previousSub.ID) + if err != nil { + return nil, stacktrace.Propagate(err, "Unable to delete previous implicit Subscription") + } + } + + notifyVolume, err := computeNotificationVolume(old, validParams.UExtent) + if err != nil { + return nil, stacktrace.Propagate(err, "Failed to compute notification volume") + } + + // Notify relevant Subscriptions + subsToNotify, err := repo.IncrementNotificationIndicesForOperationalIntents(ctx, notifyVolume) + if err != nil { + return nil, stacktrace.Propagate(err, "Failed to notify relevant Subscriptions") + } + + // Return response to client + return &PutOperationalIntentReferenceResult{ + Response: &restapi.ChangeOperationalIntentReferenceResponse{ + OperationalIntentReference: *op.ToRest(), + Subscribers: makeSubscribersToNotify(subsToNotify), + }, + }, nil +} diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go index 3e8cce828..2d42659c5 100644 --- a/pkg/scd/operational_intents_handler.go +++ b/pkg/scd/operational_intents_handler.go @@ -2,7 +2,6 @@ package scd import ( "context" - "time" "github.com/interuss/dss/pkg/api" restapi "github.com/interuss/dss/pkg/api/scdv1" @@ -12,6 +11,7 @@ import ( scdmodels "github.com/interuss/dss/pkg/scd/models" "github.com/interuss/dss/pkg/scd/repos" dssstore "github.com/interuss/dss/pkg/store" + "github.com/interuss/dss/pkg/timestamp" "github.com/interuss/stacktrace" ) @@ -138,8 +138,18 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}} } + validParams, err := actions.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, "", req.Body, a.AllowHTTPBaseUrls) + if err != nil { + return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ + Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters"))}} + } + _, err = actions.CheckUpsertPermissionsAndReturnManager(&req.Auth, validParams.State) + if err != nil { + return restapi.CreateOperationalIntentReferenceResponseSet{Response403: &restapi.ErrorResponse{ + Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.PermissionDenied, "Caller is not allowed to upsert with the requested state"))}} + } - respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, time.Now(), &req.Auth, req.Entityid, "", req.Body) + result, err := dssstore.TransactWithResult[repos.Repository, *actions.PutOperationalIntentReferenceResult](ctx, a.Store, req) if err != nil { err = stacktrace.Propagate(err, "Could not put Operational Intent Reference") errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)} @@ -152,14 +162,14 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest return restapi.CreateOperationalIntentReferenceResponseSet{Response409: &restapi.AirspaceConflictResponse{ Message: dsserr.Handle(ctx, err)}} case dsserr.MissingOVNs: - return restapi.CreateOperationalIntentReferenceResponseSet{Response409: respConflict} + return restapi.CreateOperationalIntentReferenceResponseSet{Response409: result.Conflict} default: return restapi.CreateOperationalIntentReferenceResponseSet{Response500: &api.InternalServerErrorBody{ ErrorMessage: *dsserr.Handle(ctx, stacktrace.Propagate(err, "Got an unexpected error"))}} } } - return restapi.CreateOperationalIntentReferenceResponseSet{Response201: respOK} + return restapi.CreateOperationalIntentReferenceResponseSet{Response201: result.Response} } func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *restapi.UpdateOperationalIntentReferenceRequest, @@ -169,10 +179,20 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}} } + validParams, err := actions.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, req.Ovn, req.Body, a.AllowHTTPBaseUrls) + if err != nil { + return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ + Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters"))}} + } + _, err = actions.CheckUpsertPermissionsAndReturnManager(&req.Auth, validParams.State) + if err != nil { + return restapi.UpdateOperationalIntentReferenceResponseSet{Response403: &restapi.ErrorResponse{ + Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.PermissionDenied, "Caller is not allowed to upsert with the requested state"))}} + } - respOK, respConflict, err := a.upsertOperationalIntentReference(ctx, time.Now(), &req.Auth, req.Entityid, req.Ovn, req.Body) + result, err := dssstore.TransactWithResult[repos.Repository, *actions.PutOperationalIntentReferenceResult](ctx, a.Store, req) if err != nil { - err = stacktrace.Propagate(err, "Could not put subscription") + err = stacktrace.Propagate(err, "Could not put Operational Intent Reference") errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)} switch stacktrace.GetCode(err) { case dsserr.PermissionDenied: @@ -183,195 +203,12 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest return restapi.UpdateOperationalIntentReferenceResponseSet{Response409: &restapi.AirspaceConflictResponse{ Message: dsserr.Handle(ctx, err)}} case dsserr.MissingOVNs: - return restapi.UpdateOperationalIntentReferenceResponseSet{Response409: respConflict} + return restapi.UpdateOperationalIntentReferenceResponseSet{Response409: result.Conflict} default: return restapi.UpdateOperationalIntentReferenceResponseSet{Response500: &api.InternalServerErrorBody{ ErrorMessage: *dsserr.Handle(ctx, stacktrace.Propagate(err, "Got an unexpected error"))}} } } - return restapi.UpdateOperationalIntentReferenceResponseSet{Response200: respOK} -} - -// upsertOperationalIntentReference inserts or updates an Operational Intent. -// If the ovn argument is empty (""), it will attempt to create a new Operational Intent. -func (a *Server) upsertOperationalIntentReference(ctx context.Context, now time.Time, authorizedManager *api.AuthorizationResult, entityid restapi.EntityID, ovn restapi.EntityOVN, params *restapi.PutOperationalIntentReferenceParameters, -) (*restapi.ChangeOperationalIntentReferenceResponse, *restapi.AirspaceConflictResponse, error) { - // Note: validateAndReturnOIRUpsertParams and checkUpsertPermissionsAndReturnManager could be moved out of this method and only the valid params passed, - // but this requires some changes in the caller that go beyond the immediate scope of #1088 and can be done later. - validParams, err := actions.ValidateAndReturnOIRUpsertParams(now, entityid, ovn, params, a.AllowHTTPBaseUrls) - if err != nil { - return nil, nil, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters") - } - manager, err := actions.CheckUpsertPermissionsAndReturnManager(authorizedManager, validParams.State) - if err != nil { - return nil, nil, stacktrace.PropagateWithCode(err, dsserr.PermissionDenied, "Caller is not allowed to upsert with the requested state") - } - - var responseOK *restapi.ChangeOperationalIntentReferenceResponse - var responseConflict *restapi.AirspaceConflictResponse - action := func(ctx context.Context, r repos.Repository) (err error) { - - // Get existing OperationalIntent, if any - old, err := r.GetOperationalIntent(ctx, validParams.ID) - if err != nil { - return stacktrace.Propagate(err, "Could not get OperationalIntent from repo") - } - - // Lock subscriptions based on the cell and subscriptions we're going to use - // to reduce the number of retries under concurrent load. - // See issue #1002 for details. - var subscriptionIds = make([]dssmodels.ID, 0) - - if old != nil && old.SubscriptionID != nil { - subscriptionIds = append(subscriptionIds, *old.SubscriptionID) - } - - if !validParams.SubscriptionID.Empty() { - subscriptionIds = append(subscriptionIds, validParams.SubscriptionID) - } - - err = r.LockSubscriptionsOnCells(ctx, validParams.Cells, subscriptionIds, validParams.UExtent.StartTime, validParams.UExtent.EndTime) - if err != nil { - return stacktrace.Propagate(err, "Unable to acquire lock") - } - - // Validate the request against the previous OIR - if err := actions.ValidateUpsertRequestAgainstPreviousOIR(manager, validParams.OVN, old); err != nil { - return stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "Request validation failed") - } - - var ( - version = scdmodels.VersionNumber(1) - pastOVNs = make([]scdmodels.OVN, 0) - previousSub *scdmodels.Subscription - ) - if old != nil { - version = old.Version + 1 - pastOVNs = append(old.PastOVNs, validParams.OVN) - - // Fetch the previous OIR's subscription if it exists - if old.SubscriptionID != nil { - previousSub, err = r.GetSubscription(ctx, *old.SubscriptionID) - if err != nil { - return stacktrace.Propagate(err, "Unable to get OperationalIntent's Subscription from repo") - } - } - } - - // Determine if the previous subscription is being replaced and if it will need to be cleaned up - previousSubIsBeingReplaced := previousSub != nil && validParams.SubscriptionID != previousSub.ID - removePreviousImplicitSubscription := false - if previousSubIsBeingReplaced { - removePreviousImplicitSubscription, err = actions.SubscriptionIsImplicitAndOnlyAttachedToOIR(ctx, r, validParams.ID, previousSub) - if err != nil { - return stacktrace.Propagate(err, "Could not determine if previous Subscription can be removed") - } - } - - // attachedSub is the subscription that will end up being attached to the OIR - // it defaults to the previous subscription (which may be nil), and may be updated if required by the parameters - attachedSub := previousSub - if validParams.SubscriptionID.Empty() { - // No subscription ID was provided: - // check if an implicit subscription should be created, otherwise do nothing - if validParams.ImplicitSubscription.Requested { - // Parameters for a new implicit subscription have been passed: we will create - // a new implicit subscription even if another subscription was attached to this OIR before, - // regardless of whether it was an implicit subscription or not. - if attachedSub, err = actions.CreateAndStoreNewImplicitSubscription(ctx, r, manager, validParams); err != nil { - return stacktrace.Propagate(err, "Failed to create implicit subscription") - } - } else { - // If no subscription ID is provided and no implicit subscription is requested, - // the OIR should have no attached subscription - attachedSub = nil - } - } else { - // Attempt to rely on the specified subscription - // If it is different from the previous subscription, we need to fetch it from the store - // in order to ensure it correctly covers the OIR. - // We do the check below in order to avoid re-fetching the subscription if it has not changed - if attachedSub == nil || previousSubIsBeingReplaced { - attachedSub, err = r.GetSubscription(ctx, validParams.SubscriptionID) - if err != nil { - return stacktrace.Propagate(err, "Unable to get requested Subscription from store") - } - if attachedSub == nil { - return stacktrace.NewErrorWithCode(dsserr.BadRequest, "Specified Subscription %s does not exist", validParams.SubscriptionID) - } - } - - // We need to confirm that it is owned by the calling manager - if attachedSub.Manager != manager { - return stacktrace.Propagate( - // We do a bit of wrapping gymnastics because the root error message will be sent in the response, - // and we don't want to include the effective manager in there. - stacktrace.NewErrorWithCode( - dsserr.PermissionDenied, "Specificed Subscription is owned by different client"), - // The propagation message will end in the logs and help with debugging. - "Subscription %s owned by %s, but %s attempted to use it for an OperationalIntent", - validParams.SubscriptionID, - attachedSub.Manager, - manager, - ) - } - - // We need to ensure the subscription covers the OIR's geo-temporal extent - attachedSub, err = actions.EnsureSubscriptionCoversOIR(ctx, r, attachedSub, validParams) - if err != nil { - return stacktrace.Propagate(err, "Failed to ensure subscription covers OIR") - } - } - - if validParams.State.RequiresKey() { - responseConflict, err = actions.ValidateKeyAndProvideConflictResponse(ctx, r, manager, validParams, attachedSub) - if err != nil { - return stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "Failed to validate key") - } - } - - // Construct the new OperationalIntent - op := validParams.ToOIR(manager, attachedSub, version, pastOVNs) - - // Upsert the OperationalIntent - op, err = r.UpsertOperationalIntent(ctx, op) - if err != nil { - return stacktrace.Propagate(err, "Failed to upsert OperationalIntent in repo") - } - - // Check if the previously attached subscription should be removed - if removePreviousImplicitSubscription { - err = r.DeleteSubscription(ctx, previousSub.ID) - if err != nil { - return stacktrace.Propagate(err, "Unable to delete previous implicit Subscription") - } - } - - notifyVolume, err := actions.ComputeNotificationVolume(old, validParams.UExtent) - if err != nil { - return stacktrace.Propagate(err, "Failed to compute notification volume") - } - - // Notify relevant Subscriptions - subsToNotify, err := r.IncrementNotificationIndicesForOperationalIntents(ctx, notifyVolume) - if err != nil { - return stacktrace.Propagate(err, "Failed to notify relevant Subscriptions") - } - - // Return response to client - responseOK = &restapi.ChangeOperationalIntentReferenceResponse{ - OperationalIntentReference: *op.ToRest(), - Subscribers: makeSubscribersToNotify(subsToNotify), - } - - return nil - } - - _, err = a.Store.Transact(ctx, dssstore.NewFuncOperation(action)) - if err != nil { - return nil, responseConflict, err // No need to Propagate this error as this is not a useful stacktrace line - } - - return responseOK, responseConflict, nil + return restapi.UpdateOperationalIntentReferenceResponseSet{Response200: result.Response} } diff --git a/pkg/scd/server.go b/pkg/scd/server.go index a8eab0aaa..9dea0d488 100644 --- a/pkg/scd/server.go +++ b/pkg/scd/server.go @@ -1,32 +1,9 @@ package scd import ( - restapi "github.com/interuss/dss/pkg/api/scdv1" - scdmodels "github.com/interuss/dss/pkg/scd/models" scdstore "github.com/interuss/dss/pkg/scd/store" ) -func makeSubscribersToNotify(subscriptions []*scdmodels.Subscription) []restapi.SubscriberToNotify { - result := []restapi.SubscriberToNotify{} - - subscriptionsByURL := map[string][]restapi.SubscriptionState{} - for _, sub := range subscriptions { - subState := restapi.SubscriptionState{ - SubscriptionId: restapi.SubscriptionID(sub.ID.String()), - NotificationIndex: restapi.SubscriptionNotificationIndex(sub.NotificationIndex), - } - subscriptionsByURL[sub.USSBaseURL] = append(subscriptionsByURL[sub.USSBaseURL], subState) - } - for url, states := range subscriptionsByURL { - result = append(result, restapi.SubscriberToNotify{ - UssBaseUrl: restapi.SubscriptionUssBaseURL(url), - Subscriptions: states, - }) - } - - return result -} - // Server implements scdv1.Implementation. type Server struct { Store scdstore.Store diff --git a/pkg/store/store.go b/pkg/store/store.go index 4b87b0a67..7102c7e95 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -54,17 +54,18 @@ func DecodeJSON[T OperationRequest](buf []byte) (OperationRequest, error) { } // TransactWithResult wraps Store.Transact and casts the result to ResultType, avoiding a cast at every call site. +// The cast is attempted even when Transact returns an error, since some operations intentionally return +// a partial result alongside an error (e.g. a conflict response). func TransactWithResult[R any, ResultType any](ctx context.Context, store Store[R], request OperationRequest) (ResultType, error) { var empty ResultType transactionResult, err := store.Transact(ctx, request) + if resultType, ok := transactionResult.(ResultType); ok { + return resultType, err + } if err != nil { return empty, err } - resultType, ok := transactionResult.(ResultType) - if !ok { - return empty, stacktrace.NewError("unexpected result type %T, want %T", transactionResult, empty) - } - return resultType, nil + return empty, stacktrace.NewError("unexpected result type %T, want %T", transactionResult, empty) } // FuncOperation wraps a closure as an OperationRequest for gradual migration. From 0c04bf8cf71b536f3de8ab60a37dd91ce2ae2ff4 Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Wed, 9 Sep 2026 16:50:48 +0200 Subject: [PATCH 2/6] [scd] Return conflict without error --- pkg/errors/errors.go | 4 ---- pkg/scd/actions/operational_intents.go | 16 ++++++++-------- pkg/scd/operational_intents_handler.go | 12 ++++++++---- pkg/store/store.go | 8 +++----- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index f525e3b90..ccf10b2aa 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -16,10 +16,6 @@ const ( // larger than the max area allowed. See geo/s2.go. AreaTooLarge = stacktrace.ErrorCode(iota) - // MissingOVNs is the error to signal that an AirspaceConflictResponse should - // be returned rather than the standard error response. - MissingOVNs - // AlreadyExists is used when attempting to create a resource that already // exists. AlreadyExists diff --git a/pkg/scd/actions/operational_intents.go b/pkg/scd/actions/operational_intents.go index ab94f5a34..29272a649 100644 --- a/pkg/scd/actions/operational_intents.go +++ b/pkg/scd/actions/operational_intents.go @@ -517,7 +517,7 @@ func createAndStoreNewImplicitSubscription(ctx context.Context, r repos.Reposito // validateKeyAndProvideConflictResponse ensures that the provided key contains all the necessary OVNs relevant for the area covered by the OperationalIntent. // - If all required keys are provided, (nil, nil) will be returned. -// - If keys are missing, the conflict response to be sent back as well as an error with the dsserr.MissingOVNs code will be returned. +// - If keys are missing, the conflict response will be returned (conflict, nil). // - In case of any other error, (nil, error) will be returned. func validateKeyAndProvideConflictResponse( ctx context.Context, @@ -589,7 +589,7 @@ func validateKeyAndProvideConflictResponse( } } - return responseConflict, stacktrace.NewErrorWithCode(dsserr.MissingOVNs, "Missing OVNs: %v", msg) + return responseConflict, nil } return nil, nil @@ -637,7 +637,7 @@ func ensureSubscriptionCoversOIR(ctx context.Context, r repos.Repository, sub *s } // PutOperationalIntentReferenceResult is the result of an Operational Intent Reference put operation. -// Exactly one of Response or Conflict is set: Conflict is set when the upsert failed because of missing OVNs (a dsserr.MissingOVNs error is returned alongside it) +// Exactly one of Response or Conflict is set: Conflict is set when the upsert failed because of missing OVNs, // and Response is set on success. type PutOperationalIntentReferenceResult struct { Response *restapi.ChangeOperationalIntentReferenceResponse @@ -788,13 +788,13 @@ func ExecutePutOperationalIntentReference(ctx context.Context, repo repos.Reposi } } - var responseConflict *restapi.AirspaceConflictResponse if validParams.State.RequiresKey() { - responseConflict, err = validateKeyAndProvideConflictResponse(ctx, repo, manager, validParams, attachedSub) + responseConflict, err := validateKeyAndProvideConflictResponse(ctx, repo, manager, validParams, attachedSub) if err != nil { - // responseConflict is non-nil here on a dsserr.MissingOVNs error: return it alongside - // the error so the handler can still send it to the client. See the doc comment above. - return &PutOperationalIntentReferenceResult{Conflict: responseConflict}, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "Failed to validate key") + return nil, stacktrace.Propagate(err, "Failed to validate key") + } + if responseConflict != nil { + return &PutOperationalIntentReferenceResult{Conflict: responseConflict}, nil } } diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go index 2d42659c5..026969992 100644 --- a/pkg/scd/operational_intents_handler.go +++ b/pkg/scd/operational_intents_handler.go @@ -161,14 +161,16 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest case dsserr.VersionMismatch: return restapi.CreateOperationalIntentReferenceResponseSet{Response409: &restapi.AirspaceConflictResponse{ Message: dsserr.Handle(ctx, err)}} - case dsserr.MissingOVNs: - return restapi.CreateOperationalIntentReferenceResponseSet{Response409: result.Conflict} default: return restapi.CreateOperationalIntentReferenceResponseSet{Response500: &api.InternalServerErrorBody{ ErrorMessage: *dsserr.Handle(ctx, stacktrace.Propagate(err, "Got an unexpected error"))}} } } + if result.Conflict != nil { + return restapi.CreateOperationalIntentReferenceResponseSet{Response409: result.Conflict} + } + return restapi.CreateOperationalIntentReferenceResponseSet{Response201: result.Response} } @@ -202,13 +204,15 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest case dsserr.VersionMismatch: return restapi.UpdateOperationalIntentReferenceResponseSet{Response409: &restapi.AirspaceConflictResponse{ Message: dsserr.Handle(ctx, err)}} - case dsserr.MissingOVNs: - return restapi.UpdateOperationalIntentReferenceResponseSet{Response409: result.Conflict} default: return restapi.UpdateOperationalIntentReferenceResponseSet{Response500: &api.InternalServerErrorBody{ ErrorMessage: *dsserr.Handle(ctx, stacktrace.Propagate(err, "Got an unexpected error"))}} } } + if result.Conflict != nil { + return restapi.UpdateOperationalIntentReferenceResponseSet{Response409: result.Conflict} + } + return restapi.UpdateOperationalIntentReferenceResponseSet{Response200: result.Response} } diff --git a/pkg/store/store.go b/pkg/store/store.go index 7102c7e95..405ae8df2 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -54,17 +54,15 @@ func DecodeJSON[T OperationRequest](buf []byte) (OperationRequest, error) { } // TransactWithResult wraps Store.Transact and casts the result to ResultType, avoiding a cast at every call site. -// The cast is attempted even when Transact returns an error, since some operations intentionally return -// a partial result alongside an error (e.g. a conflict response). func TransactWithResult[R any, ResultType any](ctx context.Context, store Store[R], request OperationRequest) (ResultType, error) { var empty ResultType transactionResult, err := store.Transact(ctx, request) - if resultType, ok := transactionResult.(ResultType); ok { - return resultType, err - } if err != nil { return empty, err } + if resultType, ok := transactionResult.(ResultType); ok { + return resultType, nil + } return empty, stacktrace.NewError("unexpected result type %T, want %T", transactionResult, empty) } From 8fe495a142f577dd8ac0a48d1be619b211893957 Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Fri, 28 Aug 2026 11:10:34 +0200 Subject: [PATCH 3/6] [raftstore] Rename actions packages to operations --- pkg/rid/{actions => operations}/registry.go | 2 +- pkg/rid/{actions => operations}/subscription.go | 2 +- pkg/rid/store/raftstore/store.go | 6 +++--- pkg/rid/store/sqlstore/store.go | 4 ++-- pkg/scd/constraints_handler.go | 4 ++-- pkg/scd/operational_intents_handler.go | 14 +++++++------- pkg/scd/{actions => operations}/availability.go | 2 +- pkg/scd/{actions => operations}/constraint.go | 2 +- .../{actions => operations}/operational_intents.go | 2 +- pkg/scd/{actions => operations}/registry.go | 2 +- pkg/scd/{actions => operations}/subscription.go | 2 +- pkg/scd/store/raftstore/store.go | 6 +++--- pkg/scd/store/sqlstore/store.go | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) rename pkg/rid/{actions => operations}/registry.go (92%) rename pkg/rid/{actions => operations}/subscription.go (99%) rename pkg/scd/{actions => operations}/availability.go (99%) rename pkg/scd/{actions => operations}/constraint.go (99%) rename pkg/scd/{actions => operations}/operational_intents.go (99%) rename pkg/scd/{actions => operations}/registry.go (98%) rename pkg/scd/{actions => operations}/subscription.go (99%) diff --git a/pkg/rid/actions/registry.go b/pkg/rid/operations/registry.go similarity index 92% rename from pkg/rid/actions/registry.go rename to pkg/rid/operations/registry.go index 2cd22af51..989928349 100644 --- a/pkg/rid/actions/registry.go +++ b/pkg/rid/operations/registry.go @@ -1,4 +1,4 @@ -package actions +package operations import ( "github.com/interuss/dss/pkg/rid/repos" diff --git a/pkg/rid/actions/subscription.go b/pkg/rid/operations/subscription.go similarity index 99% rename from pkg/rid/actions/subscription.go rename to pkg/rid/operations/subscription.go index e3537b402..72632ff27 100644 --- a/pkg/rid/actions/subscription.go +++ b/pkg/rid/operations/subscription.go @@ -1,4 +1,4 @@ -package actions +package operations import ( "context" diff --git a/pkg/rid/store/raftstore/store.go b/pkg/rid/store/raftstore/store.go index 6dabdecf7..00b4bf0c5 100644 --- a/pkg/rid/store/raftstore/store.go +++ b/pkg/rid/store/raftstore/store.go @@ -6,7 +6,7 @@ import ( "github.com/interuss/dss/pkg/memstore" "github.com/interuss/dss/pkg/raftstore" "github.com/interuss/dss/pkg/raftstore/consensus" - "github.com/interuss/dss/pkg/rid/actions" + "github.com/interuss/dss/pkg/rid/operations" "github.com/interuss/dss/pkg/rid/repos" ridmemstore "github.com/interuss/dss/pkg/rid/store/memstore" ridraftparams "github.com/interuss/dss/pkg/rid/store/raftstore/params" @@ -33,7 +33,7 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. } r := &repo{memStore: memStore, memRepo: memStore.GetRepo()} - store, err := raftstore.Init(ctx, logger.With(zap.String("service", "rid")), locality, params, r, actions.Registry) + store, err := raftstore.Init(ctx, logger.With(zap.String("service", "rid")), locality, params, r, operations.Registry) if err != nil { return nil, stacktrace.Propagate(err, "failed to initialize rid raftstore") } @@ -57,7 +57,7 @@ func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, err switch proposal.RequestType { default: - handler, ok := actions.Registry[string(proposal.RequestType)] + handler, ok := operations.Registry[string(proposal.RequestType)] if !ok { return nil, stacktrace.NewError("unrecognized request type: %s", proposal.RequestType) } diff --git a/pkg/rid/store/sqlstore/store.go b/pkg/rid/store/sqlstore/store.go index 20bb2e026..e51eb1662 100644 --- a/pkg/rid/store/sqlstore/store.go +++ b/pkg/rid/store/sqlstore/store.go @@ -6,7 +6,7 @@ import ( dssql "github.com/interuss/dss/pkg/sql" "github.com/interuss/dss/pkg/logging" - "github.com/interuss/dss/pkg/rid/actions" + "github.com/interuss/dss/pkg/rid/operations" "github.com/interuss/dss/pkg/rid/repos" "github.com/interuss/dss/pkg/sqlstore" "github.com/interuss/dss/pkg/store/params" @@ -45,6 +45,6 @@ func Init(ctx context.Context, logger *zap.Logger, withCheckCron bool) (*sqlstor timeBasedNotificationIndex: opts.TimeBasedNotificationIndex, } }, - Registry: actions.Registry, + Registry: operations.Registry, }, withCheckCron) } diff --git a/pkg/scd/constraints_handler.go b/pkg/scd/constraints_handler.go index 055502e72..4151855fa 100644 --- a/pkg/scd/constraints_handler.go +++ b/pkg/scd/constraints_handler.go @@ -7,8 +7,8 @@ import ( restapi "github.com/interuss/dss/pkg/api/scdv1" dsserr "github.com/interuss/dss/pkg/errors" dssmodels "github.com/interuss/dss/pkg/models" - "github.com/interuss/dss/pkg/scd/actions" scdmodels "github.com/interuss/dss/pkg/scd/models" + "github.com/interuss/dss/pkg/scd/operations" "github.com/interuss/dss/pkg/scd/repos" dssstore "github.com/interuss/dss/pkg/store" "github.com/interuss/dss/pkg/timestamp" @@ -168,7 +168,7 @@ func (a *Server) UpdateConstraintReference(ctx context.Context, req *restapi.Upd // validateConstraintUpsertRequest performs the request validation that can be done ahead of the transaction. // Note that this does NOT check for anything related to access controls: any error returned should be labeled as a dsserr.BadRequest. func validateConstraintUpsertRequest(ctx context.Context, entityid restapi.EntityID, params *restapi.PutConstraintReferenceParameters, allowHTTPBaseUrls bool) error { - _, err := actions.ValidateAndReturnConstraintUpsertParams(timestamp.MustGetRequestTimestamp(ctx), entityid, params) + _, err := operations.ValidateAndReturnConstraintUpsertParams(timestamp.MustGetRequestTimestamp(ctx), entityid, params) if err != nil { return err } diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go index 026969992..eba9ff596 100644 --- a/pkg/scd/operational_intents_handler.go +++ b/pkg/scd/operational_intents_handler.go @@ -7,8 +7,8 @@ import ( restapi "github.com/interuss/dss/pkg/api/scdv1" dsserr "github.com/interuss/dss/pkg/errors" dssmodels "github.com/interuss/dss/pkg/models" - "github.com/interuss/dss/pkg/scd/actions" scdmodels "github.com/interuss/dss/pkg/scd/models" + "github.com/interuss/dss/pkg/scd/operations" "github.com/interuss/dss/pkg/scd/repos" dssstore "github.com/interuss/dss/pkg/store" "github.com/interuss/dss/pkg/timestamp" @@ -138,18 +138,18 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}} } - validParams, err := actions.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, "", req.Body, a.AllowHTTPBaseUrls) + validParams, err := operations.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, "", req.Body, a.AllowHTTPBaseUrls) if err != nil { return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters"))}} } - _, err = actions.CheckUpsertPermissionsAndReturnManager(&req.Auth, validParams.State) + _, err = operations.CheckUpsertPermissionsAndReturnManager(&req.Auth, validParams.State) if err != nil { return restapi.CreateOperationalIntentReferenceResponseSet{Response403: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.PermissionDenied, "Caller is not allowed to upsert with the requested state"))}} } - result, err := dssstore.TransactWithResult[repos.Repository, *actions.PutOperationalIntentReferenceResult](ctx, a.Store, req) + result, err := dssstore.TransactWithResult[repos.Repository, *operations.PutOperationalIntentReferenceResult](ctx, a.Store, req) if err != nil { err = stacktrace.Propagate(err, "Could not put Operational Intent Reference") errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)} @@ -181,18 +181,18 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}} } - validParams, err := actions.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, req.Ovn, req.Body, a.AllowHTTPBaseUrls) + validParams, err := operations.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, req.Ovn, req.Body, a.AllowHTTPBaseUrls) if err != nil { return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters"))}} } - _, err = actions.CheckUpsertPermissionsAndReturnManager(&req.Auth, validParams.State) + _, err = operations.CheckUpsertPermissionsAndReturnManager(&req.Auth, validParams.State) if err != nil { return restapi.UpdateOperationalIntentReferenceResponseSet{Response403: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.PermissionDenied, "Caller is not allowed to upsert with the requested state"))}} } - result, err := dssstore.TransactWithResult[repos.Repository, *actions.PutOperationalIntentReferenceResult](ctx, a.Store, req) + result, err := dssstore.TransactWithResult[repos.Repository, *operations.PutOperationalIntentReferenceResult](ctx, a.Store, req) if err != nil { err = stacktrace.Propagate(err, "Could not put Operational Intent Reference") errResp := &restapi.ErrorResponse{Message: dsserr.Handle(ctx, err)} diff --git a/pkg/scd/actions/availability.go b/pkg/scd/operations/availability.go similarity index 99% rename from pkg/scd/actions/availability.go rename to pkg/scd/operations/availability.go index 0581ddf3e..a9d2967f7 100644 --- a/pkg/scd/actions/availability.go +++ b/pkg/scd/operations/availability.go @@ -1,4 +1,4 @@ -package actions +package operations import ( "context" diff --git a/pkg/scd/actions/constraint.go b/pkg/scd/operations/constraint.go similarity index 99% rename from pkg/scd/actions/constraint.go rename to pkg/scd/operations/constraint.go index 262be05f1..2f3f3a3e1 100644 --- a/pkg/scd/actions/constraint.go +++ b/pkg/scd/operations/constraint.go @@ -1,4 +1,4 @@ -package actions +package operations import ( "context" diff --git a/pkg/scd/actions/operational_intents.go b/pkg/scd/operations/operational_intents.go similarity index 99% rename from pkg/scd/actions/operational_intents.go rename to pkg/scd/operations/operational_intents.go index 29272a649..90082834b 100644 --- a/pkg/scd/actions/operational_intents.go +++ b/pkg/scd/operations/operational_intents.go @@ -1,4 +1,4 @@ -package actions +package operations import ( "context" diff --git a/pkg/scd/actions/registry.go b/pkg/scd/operations/registry.go similarity index 98% rename from pkg/scd/actions/registry.go rename to pkg/scd/operations/registry.go index 60961f40a..c339ea535 100644 --- a/pkg/scd/actions/registry.go +++ b/pkg/scd/operations/registry.go @@ -1,4 +1,4 @@ -package actions +package operations import ( restapi "github.com/interuss/dss/pkg/api/scdv1" diff --git a/pkg/scd/actions/subscription.go b/pkg/scd/operations/subscription.go similarity index 99% rename from pkg/scd/actions/subscription.go rename to pkg/scd/operations/subscription.go index 30106ee29..909d5b378 100644 --- a/pkg/scd/actions/subscription.go +++ b/pkg/scd/operations/subscription.go @@ -1,4 +1,4 @@ -package actions +package operations import ( "context" diff --git a/pkg/scd/store/raftstore/store.go b/pkg/scd/store/raftstore/store.go index 918499c25..baa1a11ae 100644 --- a/pkg/scd/store/raftstore/store.go +++ b/pkg/scd/store/raftstore/store.go @@ -6,7 +6,7 @@ import ( "github.com/interuss/dss/pkg/memstore" "github.com/interuss/dss/pkg/raftstore" "github.com/interuss/dss/pkg/raftstore/consensus" - "github.com/interuss/dss/pkg/scd/actions" + "github.com/interuss/dss/pkg/scd/operations" "github.com/interuss/dss/pkg/scd/repos" scdmemstore "github.com/interuss/dss/pkg/scd/store/memstore" scdraftparams "github.com/interuss/dss/pkg/scd/store/raftstore/params" @@ -33,7 +33,7 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. } r := &repo{memStore: memStore, memRepo: memStore.GetRepo()} - store, err := raftstore.Init(ctx, logger.With(zap.String("service", "scd")), locality, params, r, actions.Registry) + store, err := raftstore.Init(ctx, logger.With(zap.String("service", "scd")), locality, params, r, operations.Registry) if err != nil { return nil, stacktrace.Propagate(err, "failed to initialize scd raftstore") } @@ -57,7 +57,7 @@ func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, err switch proposal.RequestType { default: - handler, ok := actions.Registry[string(proposal.RequestType)] + handler, ok := operations.Registry[string(proposal.RequestType)] if !ok { return nil, stacktrace.NewError("unrecognized request type: %s", proposal.RequestType) } diff --git a/pkg/scd/store/sqlstore/store.go b/pkg/scd/store/sqlstore/store.go index 2dd58d4c3..d4a2b510d 100644 --- a/pkg/scd/store/sqlstore/store.go +++ b/pkg/scd/store/sqlstore/store.go @@ -6,7 +6,7 @@ import ( dssql "github.com/interuss/dss/pkg/sql" "github.com/interuss/dss/pkg/logging" - "github.com/interuss/dss/pkg/scd/actions" + "github.com/interuss/dss/pkg/scd/operations" "github.com/interuss/dss/pkg/scd/repos" "github.com/interuss/dss/pkg/sqlstore" "github.com/interuss/dss/pkg/store/params" @@ -51,6 +51,6 @@ func Init(ctx context.Context, logger *zap.Logger, withCheckCron bool) (*sqlstor version: version, } }, - Registry: actions.Registry, + Registry: operations.Registry, }, withCheckCron) } From 49b4f3ac31d59053d3350eec27adb8c04c7612e0 Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Fri, 28 Aug 2026 11:22:35 +0200 Subject: [PATCH 4/6] [raftstore] Unexport operations --- pkg/rid/operations/subscription.go | 6 +++--- pkg/scd/operations/availability.go | 8 ++++---- pkg/scd/operations/constraint.go | 20 ++++++++++---------- pkg/scd/operations/operational_intents.go | 22 +++++++++++----------- pkg/scd/operations/subscription.go | 18 +++++++++--------- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/pkg/rid/operations/subscription.go b/pkg/rid/operations/subscription.go index 72632ff27..dee5fac7e 100644 --- a/pkg/rid/operations/subscription.go +++ b/pkg/rid/operations/subscription.go @@ -16,16 +16,16 @@ func init() { Registry[ridv1.DeleteSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*ridv1.DeleteSubscriptionRequest], - Execute: ExecuteDeleteSubscription, + Execute: executeDeleteSubscription, } Registry[ridv2.DeleteSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*ridv2.DeleteSubscriptionRequest], - Execute: ExecuteDeleteSubscription, + Execute: executeDeleteSubscription, } } -func ExecuteDeleteSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeDeleteSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { var ( rawID string rawVersion string diff --git a/pkg/scd/operations/availability.go b/pkg/scd/operations/availability.go index a9d2967f7..4678b90d0 100644 --- a/pkg/scd/operations/availability.go +++ b/pkg/scd/operations/availability.go @@ -17,17 +17,17 @@ func init() { Registry[restapi.GetUssAvailabilityOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.GetUssAvailabilityRequest], - Execute: ExecuteGetUssAvailability, + Execute: executeGetUssAvailability, IsReadOnly: true, } Registry[restapi.SetUssAvailabilityOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.SetUssAvailabilityRequest], - Execute: ExecuteSetUssAvailability, + Execute: executeSetUssAvailability, } } -func ExecuteGetUssAvailability(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeGetUssAvailability(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.GetUssAvailabilityRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.GetUssAvailabilityOperationID) @@ -55,7 +55,7 @@ func ExecuteGetUssAvailability(ctx context.Context, repo repos.Repository, reque }, nil } -func ExecuteSetUssAvailability(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeSetUssAvailability(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.SetUssAvailabilityRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.SetUssAvailabilityOperationID) diff --git a/pkg/scd/operations/constraint.go b/pkg/scd/operations/constraint.go index 2f3f3a3e1..e1c2202ac 100644 --- a/pkg/scd/operations/constraint.go +++ b/pkg/scd/operations/constraint.go @@ -20,33 +20,33 @@ func init() { Registry[restapi.DeleteConstraintReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.DeleteConstraintReferenceRequest], - Execute: ExecuteDeleteConstraint, + Execute: executeDeleteConstraint, } Registry[restapi.GetConstraintReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.GetConstraintReferenceRequest], - Execute: ExecuteGetConstraint, + Execute: executeGetConstraint, IsReadOnly: true, } Registry[restapi.CreateConstraintReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.CreateConstraintReferenceRequest], - Execute: ExecutePutConstraint, + Execute: executePutConstraint, } Registry[restapi.UpdateConstraintReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.UpdateConstraintReferenceRequest], - Execute: ExecutePutConstraint, + Execute: executePutConstraint, } Registry[restapi.QueryConstraintReferencesOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.QueryConstraintReferencesRequest], - Execute: ExecuteQueryConstraintReferences, + Execute: executeQueryConstraintReferences, IsReadOnly: true, } } -func ExecuteGetConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeGetConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.GetConstraintReferenceRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.GetConstraintReferenceOperationID) @@ -75,9 +75,9 @@ func ExecuteGetConstraint(ctx context.Context, repo repos.Repository, request ds }, nil } -// ExecutePutConstraint inserts or updates a Constraint. +// executePutConstraint inserts or updates a Constraint. // If ovn is empty (""), it will attempt to create a new Constraint. -func ExecutePutConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executePutConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { var ( manager string entityid restapi.EntityID @@ -230,7 +230,7 @@ func ValidateAndReturnConstraintUpsertParams( return valid, nil } -func ExecuteQueryConstraintReferences(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeQueryConstraintReferences(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.QueryConstraintReferencesRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.QueryConstraintReferencesOperationID) @@ -270,7 +270,7 @@ func ExecuteQueryConstraintReferences(ctx context.Context, repo repos.Repository return response, nil } -func ExecuteDeleteConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeDeleteConstraint(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.DeleteConstraintReferenceRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.DeleteConstraintReferenceOperationID) diff --git a/pkg/scd/operations/operational_intents.go b/pkg/scd/operations/operational_intents.go index 90082834b..f655b4868 100644 --- a/pkg/scd/operations/operational_intents.go +++ b/pkg/scd/operations/operational_intents.go @@ -22,29 +22,29 @@ func init() { Registry[restapi.GetOperationalIntentReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.GetOperationalIntentReferenceRequest], - Execute: ExecuteGetOperationalIntentReference, + Execute: executeGetOperationalIntentReference, IsReadOnly: true, } Registry[restapi.QueryOperationalIntentReferencesOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.QueryOperationalIntentReferencesRequest], - Execute: ExecuteQueryOperationalIntentReferences, + Execute: executeQueryOperationalIntentReferences, IsReadOnly: true, } Registry[restapi.DeleteOperationalIntentReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.DeleteOperationalIntentReferenceRequest], - Execute: ExecuteDeleteOperationalIntentReference, + Execute: executeDeleteOperationalIntentReference, } Registry[restapi.CreateOperationalIntentReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.CreateOperationalIntentReferenceRequest], - Execute: ExecutePutOperationalIntentReference, + Execute: executePutOperationalIntentReference, } Registry[restapi.UpdateOperationalIntentReferenceOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.UpdateOperationalIntentReferenceRequest], - Execute: ExecutePutOperationalIntentReference, + Execute: executePutOperationalIntentReference, } } @@ -79,9 +79,9 @@ func SubscriptionIsImplicitAndOnlyAttachedToOIR(ctx context.Context, r repos.Rep return false, nil } -// ExecuteDeleteOperationalIntentReference deletes a single operational intent ref for a given ID +// executeDeleteOperationalIntentReference deletes a single operational intent ref for a given ID // at the specified version. -func ExecuteDeleteOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeDeleteOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.DeleteOperationalIntentReferenceRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.DeleteOperationalIntentReferenceOperationID) @@ -182,7 +182,7 @@ func ExecuteDeleteOperationalIntentReference(ctx context.Context, repo repos.Rep }, nil } -func ExecuteGetOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeGetOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.GetOperationalIntentReferenceRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.GetOperationalIntentReferenceOperationID) @@ -210,7 +210,7 @@ func ExecuteGetOperationalIntentReference(ctx context.Context, repo repos.Reposi }, nil } -func ExecuteQueryOperationalIntentReferences(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeQueryOperationalIntentReferences(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.QueryOperationalIntentReferencesRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.QueryOperationalIntentReferencesOperationID) @@ -644,9 +644,9 @@ type PutOperationalIntentReferenceResult struct { Conflict *restapi.AirspaceConflictResponse } -// ExecutePutOperationalIntentReference inserts or updates an Operational Intent. +// executePutOperationalIntentReference inserts or updates an Operational Intent. // If the ovn argument is empty (""), it will attempt to create a new Operational Intent. -func ExecutePutOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executePutOperationalIntentReference(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { var ( entityid restapi.EntityID ovn restapi.EntityOVN diff --git a/pkg/scd/operations/subscription.go b/pkg/scd/operations/subscription.go index 909d5b378..01585719a 100644 --- a/pkg/scd/operations/subscription.go +++ b/pkg/scd/operations/subscription.go @@ -19,33 +19,33 @@ func init() { Registry[restapi.CreateSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.CreateSubscriptionRequest], - Execute: ExecutePutSubscription, + Execute: executePutSubscription, } Registry[restapi.UpdateSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.UpdateSubscriptionRequest], - Execute: ExecutePutSubscription, + Execute: executePutSubscription, } Registry[restapi.DeleteSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.DeleteSubscriptionRequest], - Execute: ExecuteDeleteSubscription, + Execute: executeDeleteSubscription, } Registry[restapi.GetSubscriptionOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.GetSubscriptionRequest], - Execute: ExecuteGetSubscription, + Execute: executeGetSubscription, IsReadOnly: true, } Registry[restapi.QuerySubscriptionsOperationID] = dssstore.OperationHandler[repos.Repository]{ Encode: dssstore.EncodeJSON, Decode: dssstore.DecodeJSON[*restapi.QuerySubscriptionsRequest], - Execute: ExecuteQuerySubscriptions, + Execute: executeQuerySubscriptions, IsReadOnly: true, } } -func ExecutePutSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executePutSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { var ( manager string subscriptionid restapi.SubscriptionID @@ -253,7 +253,7 @@ func getOperations(ctx context.Context, r repos.Repository, opIDs []dssmodels.ID return res, nil } -func ExecuteDeleteSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeDeleteSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.DeleteSubscriptionRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.DeleteSubscriptionOperationID) @@ -305,7 +305,7 @@ func ExecuteDeleteSubscription(ctx context.Context, repo repos.Repository, reque return &restapi.DeleteSubscriptionResponse{Subscription: *p}, nil } -func ExecuteGetSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeGetSubscription(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.GetSubscriptionRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.GetSubscriptionOperationID) @@ -349,7 +349,7 @@ func ExecuteGetSubscription(ctx context.Context, repo repos.Repository, request return &restapi.GetSubscriptionResponse{Subscription: *p}, nil } -func ExecuteQuerySubscriptions(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { +func executeQuerySubscriptions(ctx context.Context, repo repos.Repository, request dssstore.OperationRequest) (any, error) { req, ok := request.(*restapi.QuerySubscriptionsRequest) if !ok { return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.QuerySubscriptionsOperationID) From e724a41e4064969cab2e3f18ba2c99da575792e2 Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Fri, 28 Aug 2026 13:09:19 +0200 Subject: [PATCH 5/6] [raftstore] Rename context methods --- cmds/core-service/main.go | 4 +-- pkg/aux_/pool_participants.go | 2 +- pkg/aux_/store/memstore/dss.go | 2 +- pkg/aux_/store/memstore/dss_test.go | 8 +++--- pkg/aux_/store/memstore/snapshot_test.go | 4 +-- pkg/aux_/store/memstore/store_test.go | 4 +-- pkg/locality/locality.go | 20 +++++++------- pkg/raftstore/consensus/proposal.go | 2 +- pkg/raftstore/store.go | 4 +-- .../memstore/identification_service_area.go | 4 +-- .../identification_service_area_test.go | 16 ++++++------ pkg/rid/store/memstore/snapshot_test.go | 4 +-- pkg/rid/store/memstore/store_test.go | 6 ++--- pkg/rid/store/memstore/subscriptions.go | 10 +++---- pkg/rid/store/memstore/subscriptions_test.go | 24 ++++++++--------- pkg/scd/constraints_handler.go | 2 +- pkg/scd/operational_intents_handler.go | 4 +-- pkg/scd/operations/constraint.go | 2 +- pkg/scd/operations/operational_intents.go | 2 +- pkg/scd/operations/subscription.go | 4 +-- pkg/scd/store/memstore/availability.go | 2 +- pkg/scd/store/memstore/constraints.go | 2 +- pkg/scd/store/memstore/operational_intents.go | 2 +- pkg/scd/store/memstore/store_test.go | 2 +- pkg/scd/store/memstore/subscriptions.go | 2 +- pkg/timestamp/timestamp.go | 26 +++++++++---------- 26 files changed, 82 insertions(+), 82 deletions(-) diff --git a/cmds/core-service/main.go b/cmds/core-service/main.go index 6b4026fda..91ee224b1 100644 --- a/cmds/core-service/main.go +++ b/cmds/core-service/main.go @@ -368,9 +368,9 @@ func RunHTTPServer(ctx context.Context, ctxCanceler func(), address, locality st handler = authorizer.TokenMiddleware(handler) handler = http.TimeoutHandler(handler, *timeout, "request timeout") handler = logging.HTTPMiddleware(logger, *dumpRequests, handler) - handler = timestamp.RequestTimestampMiddleware(handler) + handler = timestamp.Middleware(handler) handler = random.Middleware(handler) - handler = requestlocality.LocalityMiddleware(locality)(handler) + handler = requestlocality.Middleware(locality)(handler) if *enableMetrics || *enableTracing { // We use the default settings; the APIRouter handler will override the span value accordingly, as it has more information. diff --git a/pkg/aux_/pool_participants.go b/pkg/aux_/pool_participants.go index 8a00c9f1f..dae417936 100644 --- a/pkg/aux_/pool_participants.go +++ b/pkg/aux_/pool_participants.go @@ -96,7 +96,7 @@ func (a *Server) PutDSSInstancesHeartbeat(ctx context.Context, req *restapi.PutD } heartbeat.Timestamp = &ts } else { - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) heartbeat.Timestamp = &now } diff --git a/pkg/aux_/store/memstore/dss.go b/pkg/aux_/store/memstore/dss.go index ef90996f3..359beec9e 100644 --- a/pkg/aux_/store/memstore/dss.go +++ b/pkg/aux_/store/memstore/dss.go @@ -11,7 +11,7 @@ import ( ) func (r *repo) SaveOwnMetadata(ctx context.Context, loc string, publicEndpoint string) error { - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) r.state.Participants[locality(loc)] = &participant{ PublicEndpoint: publicEndpoint, diff --git a/pkg/aux_/store/memstore/dss_test.go b/pkg/aux_/store/memstore/dss_test.go index f39bcb248..4c9a730a4 100644 --- a/pkg/aux_/store/memstore/dss_test.go +++ b/pkg/aux_/store/memstore/dss_test.go @@ -15,7 +15,7 @@ var fakeClock = clockwork.NewFakeClock() func TestSaveOwnMetadataRoundTrip(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) r := newRepo() require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://example.com")) @@ -35,7 +35,7 @@ func TestSaveOwnMetadataRoundTrip(t *testing.T) { func TestSaveOwnMetadataUpsert(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) r := newRepo() require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://old.example.com")) @@ -50,7 +50,7 @@ func TestSaveOwnMetadataUpsert(t *testing.T) { func TestGetDSSMetadataPicksLatestHeartbeat(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) r := newRepo() require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://example.com")) @@ -71,7 +71,7 @@ func TestGetDSSMetadataPicksLatestHeartbeat(t *testing.T) { func TestGetDSSMetadataUpdatesHeartbeatPerSource(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) r := newRepo() require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://example.com")) diff --git a/pkg/aux_/store/memstore/snapshot_test.go b/pkg/aux_/store/memstore/snapshot_test.go index cd2f03a9f..1ff9015b4 100644 --- a/pkg/aux_/store/memstore/snapshot_test.go +++ b/pkg/aux_/store/memstore/snapshot_test.go @@ -17,7 +17,7 @@ import ( func TestSnapshotRoundTrip(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) src := newRepo() require.NoError(t, src.SaveOwnMetadata(ctx, "dss-1", "https://example.com")) ts := time.Now().UTC() @@ -40,7 +40,7 @@ func TestSnapshotRoundTrip(t *testing.T) { func TestRestoreFromSnapshotReplacesState(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) src := newRepo() require.NoError(t, src.SaveOwnMetadata(ctx, "dss-1", "https://example.com")) data, err := src.GetSnapshot() diff --git a/pkg/aux_/store/memstore/store_test.go b/pkg/aux_/store/memstore/store_test.go index 1526efb04..70376edb3 100644 --- a/pkg/aux_/store/memstore/store_test.go +++ b/pkg/aux_/store/memstore/store_test.go @@ -10,7 +10,7 @@ import ( func TestCheckpointRestore(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) r := newRepo() @@ -34,7 +34,7 @@ func TestCheckpointRestore(t *testing.T) { func TestCheckpointIsolatesUpsert(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) r := newRepo() require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://old.example.com")) diff --git a/pkg/locality/locality.go b/pkg/locality/locality.go index 8ea51faa8..6e1454c85 100644 --- a/pkg/locality/locality.go +++ b/pkg/locality/locality.go @@ -7,12 +7,12 @@ import ( "github.com/interuss/stacktrace" ) -type localityKey struct{} +type key struct{} -// MustGetRequestLocality returns the request locality from the context and panics if it is not +// MustFromContext returns the request locality from the context and panics if it is not // present, which is a programming error. -func MustGetRequestLocality(ctx context.Context) string { - locality, ok := ctx.Value(localityKey{}).(string) +func MustFromContext(ctx context.Context) string { + locality, ok := ctx.Value(key{}).(string) if !ok { panic(stacktrace.NewError("request locality not present in context")) } @@ -20,17 +20,17 @@ func MustGetRequestLocality(ctx context.Context) string { return locality } -// WithRequestLocality returns a new context with the given locality. -func WithRequestLocality(ctx context.Context, locality string) context.Context { - return context.WithValue(ctx, localityKey{}, locality) +// NewContext returns a new context with the given locality. +func NewContext(ctx context.Context, locality string) context.Context { + return context.WithValue(ctx, key{}, locality) } -// LocalityMiddleware is an HTTP middleware that stamps each incoming request with this +// Middleware is an HTTP middleware that stamps each incoming request with this // DSS instance's locality so that locality-dependent operations execute deterministically across nodes. -func LocalityMiddleware(locality string) func(http.Handler) http.Handler { +func Middleware(locality string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r.WithContext(WithRequestLocality(r.Context(), locality))) + next.ServeHTTP(w, r.WithContext(NewContext(r.Context(), locality))) }) } } diff --git a/pkg/raftstore/consensus/proposal.go b/pkg/raftstore/consensus/proposal.go index bb63fda74..332a842e8 100644 --- a/pkg/raftstore/consensus/proposal.go +++ b/pkg/raftstore/consensus/proposal.go @@ -35,7 +35,7 @@ type Proposal struct { } func (c *Consensus) newProposal(ctx context.Context, requestType RequestType, value []byte, readOnly bool) Proposal { - timestamp := timestamp.MustGetRequestTimestamp(ctx) + timestamp := timestamp.MustFromContext(ctx) seed := random.MustFromContext(ctx) return Proposal{ diff --git a/pkg/raftstore/store.go b/pkg/raftstore/store.go index a17c00827..b3c6919aa 100644 --- a/pkg/raftstore/store.go +++ b/pkg/raftstore/store.go @@ -116,8 +116,8 @@ func (s *Store[R]) processCommits(ctx context.Context, commitCh <-chan consensus continue } - proposalCtx := timestamp.WithRequestTimestamp(ctx, commit.Prop.Timestamp) - proposalCtx = locality.WithRequestLocality(proposalCtx, commit.Prop.Locality) + proposalCtx := timestamp.NewContext(ctx, commit.Prop.Timestamp) + proposalCtx = locality.NewContext(proposalCtx, commit.Prop.Locality) proposalCtx = random.NewContext(proposalCtx, commit.Prop.Seed) result, err := s.raftRepo.Apply(proposalCtx, commit.Prop) commit.Done <- consensus.ProposalResult{Result: result, Error: err} diff --git a/pkg/rid/store/memstore/identification_service_area.go b/pkg/rid/store/memstore/identification_service_area.go index bf7bb042b..02976cc69 100644 --- a/pkg/rid/store/memstore/identification_service_area.go +++ b/pkg/rid/store/memstore/identification_service_area.go @@ -61,7 +61,7 @@ func (r *repo) InsertISA(ctx context.Context, isa *ridmodels.IdentificationServi return nil, stacktrace.NewError("ISA with id %s already exists", isa.ID) } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := isaRecordFromModel(isa, now) r.state.ISAs[isa.ID] = rec @@ -77,7 +77,7 @@ func (r *repo) UpdateISA(ctx context.Context, isa *ridmodels.IdentificationServi return nil, nil } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := isaRecordFromModel(isa, now) rec.Owner = prev.Owner // It's not possible to update the owner of an ISA, this ensure it's to changed to a new value. diff --git a/pkg/rid/store/memstore/identification_service_area_test.go b/pkg/rid/store/memstore/identification_service_area_test.go index f6baa8dbf..a9bb4b34f 100644 --- a/pkg/rid/store/memstore/identification_service_area_test.go +++ b/pkg/rid/store/memstore/identification_service_area_test.go @@ -34,7 +34,7 @@ var ( func TestStoreSearchISAs(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) cells := s2.CellUnion{ s2.CellID(17106221850767130624), s2.CellID(17106221885126868992), @@ -137,7 +137,7 @@ func TestStoreSearchISAs(t *testing.T) { func TestBadVersion(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) saOut1, err := repo.InsertISA(ctx, serviceArea) @@ -159,7 +159,7 @@ func TestBadVersion(t *testing.T) { func TestStoreExpiredISA(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) saOut, err := repo.InsertISA(ctx, serviceArea) @@ -194,7 +194,7 @@ func TestStoreExpiredISA(t *testing.T) { func TestStoreDeleteISAs(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) // Insert the ISA. @@ -215,7 +215,7 @@ func TestStoreDeleteISAs(t *testing.T) { func TestStoreISAWithNoGeoData(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) endTime := fakeClock.Now().Add(24 * time.Hour) @@ -230,7 +230,7 @@ func TestStoreISAWithNoGeoData(t *testing.T) { func TestListExpiredISAs(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) // Insert ISA with endtime 1 day from now @@ -261,7 +261,7 @@ func TestListExpiredISAs(t *testing.T) { func TestListExpiredISAsWithEmptyWriter(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) // Insert ISA with endtime 1 day from now @@ -294,7 +294,7 @@ func TestListExpiredISAsWithEmptyWriter(t *testing.T) { func TestStoreCountISAs(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) // Insert the ISA. diff --git a/pkg/rid/store/memstore/snapshot_test.go b/pkg/rid/store/memstore/snapshot_test.go index a795d5f5f..50fc5423d 100644 --- a/pkg/rid/store/memstore/snapshot_test.go +++ b/pkg/rid/store/memstore/snapshot_test.go @@ -15,7 +15,7 @@ import ( func TestSnapshotRoundTrip(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) src := setUpStore(t) _, err := src.InsertISA(ctx, serviceArea) require.NoError(t, err) @@ -49,7 +49,7 @@ func TestSnapshotRoundTrip(t *testing.T) { func TestRestoreFromSnapshotReplacesState(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) src := setUpStore(t) _, err := src.InsertISA(ctx, serviceArea) require.NoError(t, err) diff --git a/pkg/rid/store/memstore/store_test.go b/pkg/rid/store/memstore/store_test.go index 62752a537..55d13081f 100644 --- a/pkg/rid/store/memstore/store_test.go +++ b/pkg/rid/store/memstore/store_test.go @@ -30,7 +30,7 @@ func setUpStore(t *testing.T) *repo { func TestDatabaseEnsuresBeginsBeforeExpires(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) var ( @@ -50,7 +50,7 @@ func TestDatabaseEnsuresBeginsBeforeExpires(t *testing.T) { func TestCheckpointRestoreISA(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) _, err := repo.InsertISA(ctx, serviceArea) @@ -76,7 +76,7 @@ func TestCheckpointRestoreISA(t *testing.T) { func TestCheckpointIsolatesNotificationIndex(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) sub, err := repo.InsertSubscription(ctx, subscriptionsPool[0].input) diff --git a/pkg/rid/store/memstore/subscriptions.go b/pkg/rid/store/memstore/subscriptions.go index 63cb22c09..f5a7bffdb 100644 --- a/pkg/rid/store/memstore/subscriptions.go +++ b/pkg/rid/store/memstore/subscriptions.go @@ -67,7 +67,7 @@ func (r *repo) InsertSubscription(ctx context.Context, s *ridmodels.Subscription return nil, stacktrace.NewError("Subscription with id %s already exists", s.ID) } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := subRecordFromModel(s, now) r.state.Subscriptions[s.ID] = rec @@ -83,7 +83,7 @@ func (r *repo) UpdateSubscription(ctx context.Context, s *ridmodels.Subscription return nil, nil } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := subRecordFromModel(s, now) rec.Owner = prev.Owner // It's not possible to update the owner of a subscription, this ensure it's to changed to a new value. @@ -137,7 +137,7 @@ func (r *repo) searchSubscriptions(ctx context.Context, cells s2.CellUnion, owne return nil, stacktrace.NewErrorWithCode(dsserr.BadRequest, "no location provided") } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) var out []*ridmodels.Subscription for rec := range r.liveSubscriptionsInCells(now, cells, owner) { @@ -154,7 +154,7 @@ func (r *repo) searchSubscriptions(ctx context.Context, cells s2.CellUnion, owne // subscription in the given cells. func (r *repo) UpdateNotificationIdxsInCells(ctx context.Context, cells s2.CellUnion) ([]*ridmodels.Subscription, error) { - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) var out []*ridmodels.Subscription for rec := range r.liveSubscriptionsInCells(now, cells, nil) { @@ -166,7 +166,7 @@ func (r *repo) UpdateNotificationIdxsInCells(ctx context.Context, cells s2.CellU func (r *repo) MaxSubscriptionCountInCellsByOwner(ctx context.Context, cells s2.CellUnion, owner dssmodels.Owner) (int, error) { - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) want := cellSet(cells) counts := make(map[s2.CellID]int, len(cells)) diff --git a/pkg/rid/store/memstore/subscriptions_test.go b/pkg/rid/store/memstore/subscriptions_test.go index 1d61cf707..993aaeff8 100644 --- a/pkg/rid/store/memstore/subscriptions_test.go +++ b/pkg/rid/store/memstore/subscriptions_test.go @@ -70,7 +70,7 @@ var ( func TestStoreGetSubscription(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) for _, r := range subscriptionsPool { @@ -90,7 +90,7 @@ func TestStoreGetSubscription(t *testing.T) { func TestStoreInsertSubscription(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) for _, r := range subscriptionsPool { @@ -134,7 +134,7 @@ func TestStoreInsertSubscription(t *testing.T) { func TestStoreDeleteSubscription(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) for _, r := range subscriptionsPool { @@ -162,7 +162,7 @@ func TestStoreDeleteSubscription(t *testing.T) { func TestStoreSearchSubscription(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now().UTC()) + ctx = timestamp.NewContext(ctx, fakeClock.Now().UTC()) repo := setUpStore(t) var ( @@ -207,7 +207,7 @@ func TestStoreSearchSubscription(t *testing.T) { func TestStoreExpiredSubscription(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) endTime := fakeClock.Now().Add(24 * time.Hour) @@ -221,7 +221,7 @@ func TestStoreExpiredSubscription(t *testing.T) { require.NoError(t, err) // The subscription's endTime is 24 hours from now. - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now().Add(23*time.Hour)) + ctx = timestamp.NewContext(ctx, fakeClock.Now().Add(23*time.Hour)) // We should still be able to find the subscription by searching and by ID. subs, err := repo.SearchSubscriptionsByOwner(ctx, sub.Cells, "original owner") @@ -233,7 +233,7 @@ func TestStoreExpiredSubscription(t *testing.T) { require.NotNil(t, &ret) // But now the subscription has expired. - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now().Add(25*time.Hour)) + ctx = timestamp.NewContext(ctx, fakeClock.Now().Add(25*time.Hour)) subs, err = repo.SearchSubscriptionsByOwner(ctx, sub.Cells, "original owner") require.NoError(t, err) @@ -246,7 +246,7 @@ func TestStoreExpiredSubscription(t *testing.T) { func TestStoreSubscriptionWithNoGeoData(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) endTime := fakeClock.Now().Add(24 * time.Hour) @@ -261,7 +261,7 @@ func TestStoreSubscriptionWithNoGeoData(t *testing.T) { func TestMaxSubscriptionCountInCellsByOwner(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) for _, s := range subscriptionsPool { @@ -276,7 +276,7 @@ func TestMaxSubscriptionCountInCellsByOwner(t *testing.T) { func TestListExpiredSubscriptions(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) fakeClock := clockwork.NewFakeClockAt(time.Now()) @@ -309,7 +309,7 @@ func TestListExpiredSubscriptions(t *testing.T) { func TestListExpiredSubscriptionsWithEmptyWriter(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) // Insert Subscription with endtime 1 day from now @@ -342,7 +342,7 @@ func TestListExpiredSubscriptionsWithEmptyWriter(t *testing.T) { func TestStoreCountSubscription(t *testing.T) { ctx := context.Background() - ctx = timestamp.WithRequestTimestamp(ctx, fakeClock.Now()) + ctx = timestamp.NewContext(ctx, fakeClock.Now()) repo := setUpStore(t) for _, r := range subscriptionsPool { diff --git a/pkg/scd/constraints_handler.go b/pkg/scd/constraints_handler.go index 4151855fa..692a5405c 100644 --- a/pkg/scd/constraints_handler.go +++ b/pkg/scd/constraints_handler.go @@ -168,7 +168,7 @@ func (a *Server) UpdateConstraintReference(ctx context.Context, req *restapi.Upd // validateConstraintUpsertRequest performs the request validation that can be done ahead of the transaction. // Note that this does NOT check for anything related to access controls: any error returned should be labeled as a dsserr.BadRequest. func validateConstraintUpsertRequest(ctx context.Context, entityid restapi.EntityID, params *restapi.PutConstraintReferenceParameters, allowHTTPBaseUrls bool) error { - _, err := operations.ValidateAndReturnConstraintUpsertParams(timestamp.MustGetRequestTimestamp(ctx), entityid, params) + _, err := operations.ValidateAndReturnConstraintUpsertParams(timestamp.MustFromContext(ctx), entityid, params) if err != nil { return err } diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go index eba9ff596..8e00f28f9 100644 --- a/pkg/scd/operational_intents_handler.go +++ b/pkg/scd/operational_intents_handler.go @@ -138,7 +138,7 @@ func (a *Server) CreateOperationalIntentReference(ctx context.Context, req *rest return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}} } - validParams, err := operations.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, "", req.Body, a.AllowHTTPBaseUrls) + validParams, err := operations.ValidateAndReturnOIRUpsertParams(timestamp.MustFromContext(ctx), req.Entityid, "", req.Body, a.AllowHTTPBaseUrls) if err != nil { return restapi.CreateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters"))}} @@ -181,7 +181,7 @@ func (a *Server) UpdateOperationalIntentReference(ctx context.Context, req *rest return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(req.BodyParseError, dsserr.BadRequest, "Malformed params"))}} } - validParams, err := operations.ValidateAndReturnOIRUpsertParams(timestamp.MustGetRequestTimestamp(ctx), req.Entityid, req.Ovn, req.Body, a.AllowHTTPBaseUrls) + validParams, err := operations.ValidateAndReturnOIRUpsertParams(timestamp.MustFromContext(ctx), req.Entityid, req.Ovn, req.Body, a.AllowHTTPBaseUrls) if err != nil { return restapi.UpdateOperationalIntentReferenceResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Operational Intent Reference upsert parameters"))}} diff --git a/pkg/scd/operations/constraint.go b/pkg/scd/operations/constraint.go index e1c2202ac..7b32dd7f5 100644 --- a/pkg/scd/operations/constraint.go +++ b/pkg/scd/operations/constraint.go @@ -94,7 +94,7 @@ func executePutConstraint(ctx context.Context, repo repos.Repository, request ds return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.CreateConstraintReferenceOperationID) } - validParams, err := ValidateAndReturnConstraintUpsertParams(timestamp.MustGetRequestTimestamp(ctx), entityid, params) + validParams, err := ValidateAndReturnConstraintUpsertParams(timestamp.MustFromContext(ctx), entityid, params) if err != nil { return nil, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to validate Constraint upsert parameters") } diff --git a/pkg/scd/operations/operational_intents.go b/pkg/scd/operations/operational_intents.go index f655b4868..76216c814 100644 --- a/pkg/scd/operations/operational_intents.go +++ b/pkg/scd/operations/operational_intents.go @@ -663,7 +663,7 @@ func executePutOperationalIntentReference(ctx context.Context, repo repos.Reposi return nil, stacktrace.NewError("unexpected request type %T for operation %q", request, restapi.CreateOperationalIntentReferenceOperationID) } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) // Base URL scheme validation is a pre-flight, request-only check performed by the handler // before this action is proposed for consensus; skip it here (allowHTTPBaseUrls: true). diff --git a/pkg/scd/operations/subscription.go b/pkg/scd/operations/subscription.go index 01585719a..a4cb4c989 100644 --- a/pkg/scd/operations/subscription.go +++ b/pkg/scd/operations/subscription.go @@ -119,7 +119,7 @@ func executePutSubscription(ctx context.Context, repo repos.Repository, request } // Validate and perhaps correct StartTime and EndTime. - if err := subreq.AdjustTimeRange(timestamp.MustGetRequestTimestamp(ctx), old); err != nil { + if err := subreq.AdjustTimeRange(timestamp.MustFromContext(ctx), old); err != nil { return nil, stacktrace.Propagate(err, "Error adjusting time range of Subscription") } @@ -373,7 +373,7 @@ func executeQuerySubscriptions(ctx context.Context, repo repos.Repository, reque return nil, stacktrace.Propagate(err, "Error searching Subscriptions in repo") } - nowMarker := timestamp.MustGetRequestTimestamp(ctx) + nowMarker := timestamp.MustFromContext(ctx) // Return response to client response := &restapi.QuerySubscriptionsResponse{ diff --git a/pkg/scd/store/memstore/availability.go b/pkg/scd/store/memstore/availability.go index bb750996a..8fe0dc839 100644 --- a/pkg/scd/store/memstore/availability.go +++ b/pkg/scd/store/memstore/availability.go @@ -26,7 +26,7 @@ func (r *repo) GetUssAvailability(_ context.Context, id dssmodels.Manager) (*scd } func (r *repo) UpsertUssAvailability(ctx context.Context, s *scdmodels.UssAvailabilityStatus) (*scdmodels.UssAvailabilityStatus, error) { - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := &availabilityRecord{ Uss: s.Uss, diff --git a/pkg/scd/store/memstore/constraints.go b/pkg/scd/store/memstore/constraints.go index 6fb54f143..33c9a0b6a 100644 --- a/pkg/scd/store/memstore/constraints.go +++ b/pkg/scd/store/memstore/constraints.go @@ -67,7 +67,7 @@ func (r *repo) UpsertConstraint(ctx context.Context, s *scdmodels.Constraint) (* return nil, stacktrace.Propagate(err, "Failed to convert array to jackc/pgtype") } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := &constraintRecord{ ID: s.ID, diff --git a/pkg/scd/store/memstore/operational_intents.go b/pkg/scd/store/memstore/operational_intents.go index fad0deec2..0abbab93f 100644 --- a/pkg/scd/store/memstore/operational_intents.go +++ b/pkg/scd/store/memstore/operational_intents.go @@ -98,7 +98,7 @@ func (r *repo) UpsertOperationalIntent(ctx context.Context, operation *scdmodels ussRequestedOVN = operation.OVN.String() } - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := &operationalIntentRecord{ ID: operation.ID, diff --git a/pkg/scd/store/memstore/store_test.go b/pkg/scd/store/memstore/store_test.go index 7f43bdf8c..caecd5bc6 100644 --- a/pkg/scd/store/memstore/store_test.go +++ b/pkg/scd/store/memstore/store_test.go @@ -40,7 +40,7 @@ func setUpStore(t *testing.T) *repo { // writeCtx returns a context carrying a deterministic write timestamp so that // updated_at is controlled in tests. func writeCtx() context.Context { - return timestamp.WithRequestTimestamp(context.Background(), writeTime) + return timestamp.NewContext(context.Background(), writeTime) } func sampleConstraint() *scdmodels.Constraint { diff --git a/pkg/scd/store/memstore/subscriptions.go b/pkg/scd/store/memstore/subscriptions.go index 02f233698..850d06125 100644 --- a/pkg/scd/store/memstore/subscriptions.go +++ b/pkg/scd/store/memstore/subscriptions.go @@ -78,7 +78,7 @@ func (r *repo) GetSubscription(_ context.Context, id dssmodels.ID) (*scdmodels.S } func (r *repo) UpsertSubscription(ctx context.Context, s *scdmodels.Subscription) (*scdmodels.Subscription, error) { - now := timestamp.MustGetRequestTimestamp(ctx) + now := timestamp.MustFromContext(ctx) rec := &subscriptionRecord{ ID: s.ID, diff --git a/pkg/timestamp/timestamp.go b/pkg/timestamp/timestamp.go index 28dbe9451..fe1329d46 100644 --- a/pkg/timestamp/timestamp.go +++ b/pkg/timestamp/timestamp.go @@ -8,13 +8,13 @@ import ( "github.com/interuss/stacktrace" ) -type timestampKey struct{} +type key struct{} -// requestTimestampFromContext returns the request timestamp from the context, or an error if the value is not present or if it is zero. +// fromContext returns the request timestamp from the context, or an error if the value is not present or if it is zero. // The timestamp is set by the Middleware when a query is received then (on the receiver side) by the Raftstore when the query is applied. // It is then used for deterministic execution of time-dependent queries. -func requestTimestampFromContext(ctx context.Context) (time.Time, error) { - timestamp, ok := ctx.Value(timestampKey{}).(time.Time) +func fromContext(ctx context.Context) (time.Time, error) { + timestamp, ok := ctx.Value(key{}).(time.Time) if !ok { return time.Time{}, stacktrace.NewError("timestamp not found in context") } @@ -26,10 +26,10 @@ func requestTimestampFromContext(ctx context.Context) (time.Time, error) { return timestamp, nil } -// MustGetRequestTimestamp returns the request timestamp from the context and panics if it is not +// MustFromContext returns the request timestamp from the context and panics if it is not // present or invalid, which is a programming error. -func MustGetRequestTimestamp(ctx context.Context) time.Time { - timestamp, err := requestTimestampFromContext(ctx) +func MustFromContext(ctx context.Context) time.Time { + timestamp, err := fromContext(ctx) if err != nil { panic(err) } @@ -37,18 +37,18 @@ func MustGetRequestTimestamp(ctx context.Context) time.Time { return timestamp } -// WithRequestTimestamp returns a new context with the given timestamp. -func WithRequestTimestamp(ctx context.Context, timestamp time.Time) context.Context { - return context.WithValue(ctx, timestampKey{}, timestamp) +// NewContext returns a new context with the given timestamp. +func NewContext(ctx context.Context, timestamp time.Time) context.Context { + return context.WithValue(ctx, key{}, timestamp) } -// RequestTimestampMiddleware is an HTTP middleware that stamps each incoming +// Middleware is an HTTP middleware that stamps each incoming // request with its received time. This timestamp is later used as the // timestamp of the Raft proposal, so that time-dependent queries // execute deterministically across nodes and contexts (catchup / restart etc.). -func RequestTimestampMiddleware(next http.Handler) http.Handler { +func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := WithRequestTimestamp(r.Context(), time.Now()) + ctx := NewContext(r.Context(), time.Now()) next.ServeHTTP(w, r.WithContext(ctx)) }) } From cf95e9c6a50c532cf9f2942c2204d186ddd1dba4 Mon Sep 17 00:00:00 2001 From: Mariem Baccari Date: Wed, 19 Aug 2026 13:51:12 +0200 Subject: [PATCH 6/6] [raftstore] Embed memstore and use checkpoint --- pkg/aux_/store/raftstore/store.go | 19 +++++-------------- pkg/raftstore/store.go | 17 ++++++++--------- pkg/rid/store/raftstore/store.go | 15 +++------------ pkg/scd/store/raftstore/store.go | 15 +++------------ 4 files changed, 19 insertions(+), 47 deletions(-) diff --git a/pkg/aux_/store/raftstore/store.go b/pkg/aux_/store/raftstore/store.go index 8f8a4270f..7b55231ed 100644 --- a/pkg/aux_/store/raftstore/store.go +++ b/pkg/aux_/store/raftstore/store.go @@ -24,8 +24,7 @@ const ( // repo is a full implementation of aux_.repos.Repository for Raft-based storage. type repo struct { consensus *consensus.Consensus - memStore *memstore.Store[repos.Repository] - memRepo repos.Repository + *memstore.Store[repos.Repository] } func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore.Store[repos.Repository], error) { @@ -39,7 +38,7 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. return nil, stacktrace.Propagate(err, "failed to initialize aux memstore") } - r := &repo{memStore: memStore, memRepo: memStore.GetRepo()} + r := &repo{Store: memStore} store, err := raftstore.Init(ctx, logger.With(zap.String("service", "aux_")), locality, params, r, nil) if err != nil { return nil, stacktrace.Propagate(err, "failed to initialize aux raftstore") @@ -52,14 +51,6 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. func (r *repo) GetRepo() repos.Repository { return r } -func (r *repo) GetSnapshot() ([]byte, error) { - return r.memStore.GetSnapshot() -} - -func (r *repo) RestoreFromSnapshot(data []byte) error { - return r.memStore.RestoreFromSnapshot(data) -} - func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, error) { switch proposal.RequestType { case saveOwnMetadata: @@ -68,10 +59,10 @@ func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, err return nil, stacktrace.Propagate(err, "failed to unmarshal %s payload", saveOwnMetadata) } - return nil, r.memRepo.SaveOwnMetadata(ctx, payload.Locality, payload.PublicEndpoint) + return nil, r.Store.GetRepo().SaveOwnMetadata(ctx, payload.Locality, payload.PublicEndpoint) case getDSSMetadata: - return r.memRepo.GetDSSMetadata(ctx) + return r.Store.GetRepo().GetDSSMetadata(ctx) case recordHeartbeat: var heartbeat auxmodels.Heartbeat @@ -79,7 +70,7 @@ func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, err return nil, stacktrace.Propagate(err, "failed to unmarshal %s payload", recordHeartbeat) } - return nil, r.memRepo.RecordHeartbeat(ctx, heartbeat) + return nil, r.Store.GetRepo().RecordHeartbeat(ctx, heartbeat) default: return nil, stacktrace.NewError("unknown request type: %q", proposal.RequestType) diff --git a/pkg/raftstore/store.go b/pkg/raftstore/store.go index b3c6919aa..436f4bf8f 100644 --- a/pkg/raftstore/store.go +++ b/pkg/raftstore/store.go @@ -5,6 +5,7 @@ import ( "github.com/interuss/dss/pkg/locality" "github.com/interuss/dss/pkg/logging" + "github.com/interuss/dss/pkg/memstore" "github.com/interuss/dss/pkg/raftstore/consensus" raftparams "github.com/interuss/dss/pkg/raftstore/params" "github.com/interuss/dss/pkg/random" @@ -15,19 +16,12 @@ import ( ) type RaftRepo[R any] interface { - GetRepo() R + memstore.MemRepo[R] + // Apply is called on every committed entry. The proposal must be applied atomically. // The any return mirrors store.OperationHandler.Execute: different requests yield different // concrete result types. Callers recover the type via store.TransactWithResult. Apply(ctx context.Context, proposal consensus.Proposal) (any, error) - - // GetSnapshot returns a serialized view of current state, suitable - // for restoring via RestoreFromSnapshot. - GetSnapshot() ([]byte, error) - - // RestoreFromSnapshot replaces all state with the snapshot in data. - // data is always the output of a prior GetSnapshot. - RestoreFromSnapshot(data []byte) error } type Store[R any] struct { @@ -119,7 +113,12 @@ func (s *Store[R]) processCommits(ctx context.Context, commitCh <-chan consensus proposalCtx := timestamp.NewContext(ctx, commit.Prop.Timestamp) proposalCtx = locality.NewContext(proposalCtx, commit.Prop.Locality) proposalCtx = random.NewContext(proposalCtx, commit.Prop.Seed) + s.raftRepo.Checkpoint() result, err := s.raftRepo.Apply(proposalCtx, commit.Prop) + if err != nil { + s.logger.Warn("failed to apply proposal, rolling back", zap.String("proposal_id", commit.Prop.ID), zap.String("proposal_type", string(commit.Prop.RequestType)), zap.Error(err)) + s.raftRepo.Restore() + } commit.Done <- consensus.ProposalResult{Result: result, Error: err} } } diff --git a/pkg/rid/store/raftstore/store.go b/pkg/rid/store/raftstore/store.go index 00b4bf0c5..cfbf6dd51 100644 --- a/pkg/rid/store/raftstore/store.go +++ b/pkg/rid/store/raftstore/store.go @@ -17,8 +17,7 @@ import ( // repo is a full implementation of rid.repos.Repository for Raft-based storage. type repo struct { consensus *consensus.Consensus - memStore *memstore.Store[repos.Repository] - memRepo repos.Repository + *memstore.Store[repos.Repository] } func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore.Store[repos.Repository], error) { @@ -32,7 +31,7 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. return nil, stacktrace.Propagate(err, "failed to initialize rid memstore") } - r := &repo{memStore: memStore, memRepo: memStore.GetRepo()} + r := &repo{Store: memStore} store, err := raftstore.Init(ctx, logger.With(zap.String("service", "rid")), locality, params, r, operations.Registry) if err != nil { return nil, stacktrace.Propagate(err, "failed to initialize rid raftstore") @@ -45,14 +44,6 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. func (r *repo) GetRepo() repos.Repository { return r } -func (r *repo) GetSnapshot() ([]byte, error) { - return r.memStore.GetSnapshot() -} - -func (r *repo) RestoreFromSnapshot(data []byte) error { - return r.memStore.RestoreFromSnapshot(data) -} - func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, error) { switch proposal.RequestType { @@ -67,6 +58,6 @@ func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, err return nil, stacktrace.Propagate(err, "failed to decode %s payload", proposal.RequestType) } - return handler.Execute(ctx, r.memRepo, request) + return handler.Execute(ctx, r.Store.GetRepo(), request) } } diff --git a/pkg/scd/store/raftstore/store.go b/pkg/scd/store/raftstore/store.go index baa1a11ae..7718c7d17 100644 --- a/pkg/scd/store/raftstore/store.go +++ b/pkg/scd/store/raftstore/store.go @@ -17,8 +17,7 @@ import ( // repo is a full implementation of scd.repos.Repository for Raft-based storage. type repo struct { consensus *consensus.Consensus - memStore *memstore.Store[repos.Repository] - memRepo repos.Repository + *memstore.Store[repos.Repository] } func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore.Store[repos.Repository], error) { @@ -32,7 +31,7 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. return nil, stacktrace.Propagate(err, "failed to initialize scd memstore") } - r := &repo{memStore: memStore, memRepo: memStore.GetRepo()} + r := &repo{Store: memStore} store, err := raftstore.Init(ctx, logger.With(zap.String("service", "scd")), locality, params, r, operations.Registry) if err != nil { return nil, stacktrace.Propagate(err, "failed to initialize scd raftstore") @@ -45,14 +44,6 @@ func Init(ctx context.Context, logger *zap.Logger, locality string) (*raftstore. func (r *repo) GetRepo() repos.Repository { return r } -func (r *repo) GetSnapshot() ([]byte, error) { - return r.memStore.GetSnapshot() -} - -func (r *repo) RestoreFromSnapshot(data []byte) error { - return r.memStore.RestoreFromSnapshot(data) -} - func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, error) { switch proposal.RequestType { @@ -67,6 +58,6 @@ func (r *repo) Apply(ctx context.Context, proposal consensus.Proposal) (any, err return nil, stacktrace.Propagate(err, "failed to decode %s payload", proposal.RequestType) } - return handler.Execute(ctx, r.memRepo, request) + return handler.Execute(ctx, r.Store.GetRepo(), request) } }