Repository: interuss/dss
Pull request #1113 (2024-09-12 14:38:03)

[dss/SCD] early subscription lock for OIR deletions
The early locking already happens for insertions/mutations, there is no reason not to do it for deletions.

Part of #814 


Patch:
diff --git a/.gitmodules b/.gitmodules
index 1a33029..542c086 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
 [submodule "interfaces/astm-utm/Protocol"]
 	path = interfaces/astm-utm/Protocol
-	url = https://github.com/astm-utm/Protocol
+	url = https://github.com/interuss/astm-utm-protocol
 [submodule "interfaces/rid/v1"]
 	path = interfaces/rid/v1
 	url = https://github.com/uastech/standards
diff --git a/Makefile b/Makefile
index 6359b31..a26c8a6 100644
--- a/Makefile
+++ b/Makefile
@@ -61,6 +61,7 @@ go-lint:
 .PHONY: terraform-lint
 terraform-lint:
 	docker run --rm -w /opt/dss -v ./deploy:/opt/dss/deploy -e TF_LOG=TRACE hashicorp/terraform fmt -recursive -check
+	./deploy/infrastructure/utils/generate_terraform_variables.sh --lint
 
 # This mirrors the hygiene-tests continuous integration workflow job (.github/workflows/ci.yml)
 .PHONY: hygiene-tests
diff --git a/cmds/core-service/main.go b/cmds/core-service/main.go
index bdd21e2..30b773c 100644
--- a/cmds/core-service/main.go
+++ b/cmds/core-service/main.go
@@ -181,7 +181,7 @@ func createSCDServer(ctx context.Context, logger *zap.Logger) (*scd.Server, erro
 		return nil, stacktrace.Propagate(err, "Failed to connect to strategic conflict detection database; verify your database configuration is current with https://github.com/interuss/dss/tree/master/build#upgrading-database-schemas")
 	}
 
-	scdStore, err := scdc.NewStore(ctx, scdCrdb, logger)
+	scdStore, err := scdc.NewStore(ctx, scdCrdb)
 	if err != nil {
 		// TODO: More robustly detect failure to create SCD server is due to a problem that may be temporary
 		if strings.Contains(err.Error(), "connect: connection refused") || strings.Contains(err.Error(), "database \"scd\" does not exist") {
diff --git a/deploy/infrastructure/utils/generate_terraform_variables.sh b/deploy/infrastructure/utils/generate_terraform_variables.sh
index 824443f..79c310e 100755
--- a/deploy/infrastructure/utils/generate_terraform_variables.sh
+++ b/deploy/infrastructure/utils/generate_terraform_variables.sh
@@ -1,13 +1,21 @@
 #!/usr/bin/env bash
-set -e
+set -eo pipefail
 
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+OS=$(uname)
+if [[ "$OS" == "Darwin" ]]; then
+	# OSX uses BSD readlink
+	BASEDIR="$(dirname "$0")"
+else
+	BASEDIR=$(readlink -e "$(dirname "$0")")
+fi
+cd "${BASEDIR}"
 
 docker build -t terraform-variables:latest .
 docker run \
-    -v "${SCRIPT_DIR}/":/app/utils:rw \
-    -v "${SCRIPT_DIR}/../dependencies":/app/examples:rw \
-    -v "${SCRIPT_DIR}/../modules":/app/modules:rw \
-    -w /app/utils \
+    -v "${BASEDIR}/":/app/infrastructure/utils:rw \
+    -v "${BASEDIR}/../dependencies":/app/infrastructure/dependencies:rw \
+    -v "${BASEDIR}/../modules":/app/infrastructure/modules:rw \
+    -v "${BASEDIR}/../../operations":/app/operations:rw \
+    -w /app/infrastructure/utils/ \
     terraform-variables \
-    ls ../ && python variables.py
\ No newline at end of file
+    /bin/bash -c "python variables.py $*"
diff --git a/deploy/infrastructure/utils/variables.py b/deploy/infrastructure/utils/variables.py
index 0811471..afc5f3d 100755
--- a/deploy/infrastructure/utils/variables.py
+++ b/deploy/infrastructure/utils/variables.py
@@ -8,6 +8,8 @@ from os import listdir
 from os.path import isfile, join, abspath, dirname, exists
 from typing import Dict, List, Tuple
 import hcl2
+import argparse
+import sys
 
 DEFINITIONS_PATH = join(abspath(dirname(__file__)), "definitions")
 GENERATED_TFVARS_MD_FILENAME = "TFVARS.gen.md"
@@ -20,10 +22,7 @@ GENERATED_COMMENT = """
 
 # Variables per project
 # For all */terraform-*
-GLOBAL_VARIABLES = [
-    "app_hostname",
-    "crdb_hostname_suffix"
-]
+GLOBAL_VARIABLES = ["app_hostname", "crdb_hostname_suffix"]
 
 # dependencies/terraform-commons-dss
 COMMONS_DSS_VARIABLES = GLOBAL_VARIABLES + [
@@ -38,14 +37,14 @@ COMMONS_DSS_VARIABLES = GLOBAL_VARIABLES + [
     "crdb_cluster_name",
     "crdb_locality",
     "crdb_external_nodes",
-    "kubernetes_namespace"
+    "kubernetes_namespace",
 ]
 
 # dependencies/terraform-*-kubernetes
 COMMON_KUBERNETES_VARIABLES = GLOBAL_VARIABLES + [
     "cluster_name",
     "node_count",
-    "kubernetes_version"
+    "kubernetes_version",
 ]
 
 # dependencies/terraform-google-kubernetes
@@ -70,7 +69,7 @@ AWS_KUBERNETES_VARIABLES = [
     "aws_region",
     "aws_instance_type",
     "aws_route53_zone_id",
-    "aws_iam_permissions_boundary"
+    "aws_iam_permissions_boundary",
 ] + COMMON_KUBERNETES_VARIABLES
 
 # modules/terraform-aws-dss
@@ -88,9 +87,7 @@ PROJECT_VARIABLES = {
     "../dependencies/terraform-aws-kubernetes": AWS_KUBERNETES_VARIABLES,
     "../dependencies/terraform-google-kubernetes": GOOGLE_KUBERNETES_VARIABLES,
     "../dependencies/terraform-commons-dss": COMMONS_DSS_VARIABLES,
-    "../../operations/ci/aws-1": list(
-        dict.fromkeys(AWS_MODULE_VARIABLES)
-    )
+    "../../operations/ci/aws-1": list(dict.fromkeys(AWS_MODULE_VARIABLES)),
 }
 
 
@@ -175,9 +172,11 @@ def comment(content: str) -> str:
     return commented_lines
 
 
-def get_variables_tf_content(variables: List[str], definitions: Dict[str, str]) -> str:
+def get_variables_gen_tf_content(
+    variables: List[str], definitions: Dict[str, str]
+) -> str:
     """
-    Generate the content of variables.tf (Terraform definitions) based
+    Generate the content of variables.gen.tf (Terraform definitions) based
     on the `variables` list. `variables` contains the variables names to
     include in the content. `definitions` contains the definitions of all
     available variables.
@@ -234,9 +233,9 @@ def write_files(definitions: Dict[str, str]):
     for path, variables in PROJECT_VARIABLES.items():
         project_name = path.split("/")[-1]
 
-        # Generate variables.tf definition
+        # Generate variables.gen.tf definition
         var_filename = join(path, GENERATED_VARIABLES_FILENAME)
-        content = get_variables_tf_content(variables, definitions)
+        content = get_variables_gen_tf_content(variables, definitions)
         write_file(var_filename, content)
 
         if is_example_project(path):
@@ -249,6 +248,73 @@ def write_files(definitions: Dict[str, str]):
             write_file(tfvars_md_filename, content)
 
 
+def read_file(file_path: str) -> str:
+    """
+    Reads a file and returns its content
+
+    Arguments:
+        file_path: the relative path of the file to be read from
+    Returns:
+        the content of the file as a str
+    """
+    with open(file_path, "r") as file:
+        content = file.read()
+    return content
+
+
+def diff_files(definitions: Dict[str, str]) -> bool:
+    """
+    Generates the `variables.gen.tf` file content in memory and diffs it
+    against the `variables.get.tf` files found on disk
+
+    Arguments:
+        definitions: a dict where the keys are the terraform variable names,
+        and the valuies are the content of that file.
+    Returns:
+        true iff the generated content and locally stored content are equal
+        string values
+    """
+    for path, variables in PROJECT_VARIABLES.items():
+        # Generate variables.gen.tf definition
+        var_filename = join(path, GENERATED_VARIABLES_FILENAME)
+        generated_content = get_variables_gen_tf_content(variables, definitions)
+
+        try:
+            actual_content = read_file(var_filename)
+        except Exception as e:
+            print(f"Error reading {var_filename}: {e}")
+            return False
+
+        if generated_content != actual_content:
+            return False
+
+    return True
+
+
 if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--lint",
+        action="store_true",
+        help="Evaluate the differences between the expected generated `variables.gen.tf` files\
+                and the ones stored locally without modifiying existing files or writing any\
+                results out to disk.\
+                Exits with code 0 on success and if there are no differences, else exits with code 1",
+    )
+    args = parser.parse_args()
+
     definitions = load_tf_definitions()
-    write_files(definitions)
+
+    if args.lint:
+        if not diff_files(definitions):
+            print(
+                "variables.py: generated content was NOT equal to local variables.gen.tf content"
+            )
+            sys.exit(1)
+        else:
+            print(
+                "variables.py: generated content was equal to local variables.gen.tf content"
+            )
+            sys.exit(0)
+    else:
+        write_files(definitions)
diff --git a/interfaces/astm-utm/Protocol b/interfaces/astm-utm/Protocol
index cb7cf96..1d3d8fb 160000
--- a/interfaces/astm-utm/Protocol
+++ b/interfaces/astm-utm/Protocol
@@ -1,1 +1,1 @@
-Subproject commit cb7cf962d3a0c01b5ab12502f5f54789624977bf
+Subproject commit 1d3d8fbe75414e23d7e19ce35955770bea5e413f
diff --git a/interfaces/openapi-to-go-server/example/api/scd/interface.gen.go b/interfaces/openapi-to-go-server/example/api/scd/interface.gen.go
index 42d9042..be2c694 100644
--- a/interfaces/openapi-to-go-server/example/api/scd/interface.gen.go
+++ b/interfaces/openapi-to-go-server/example/api/scd/interface.gen.go
@@ -7,11 +7,11 @@ import (
 )
 
 var (
-	UtmAvailabilityArbitrationScope          = api.RequiredScope("utm.availability_arbitration")
 	UtmConformanceMonitoringSaScope          = api.RequiredScope("utm.conformance_monitoring_sa")
 	UtmConstraintManagementScope             = api.RequiredScope("utm.constraint_management")
-	UtmStrategicCoordinationScope            = api.RequiredScope("utm.strategic_coordination")
 	UtmConstraintProcessingScope             = api.RequiredScope("utm.constraint_processing")
+	UtmAvailabilityArbitrationScope          = api.RequiredScope("utm.availability_arbitration")
+	UtmStrategicCoordinationScope            = api.RequiredScope("utm.strategic_coordination")
 	QueryOperationalIntentReferencesSecurity = []api.AuthorizationOption{
 		{
 			"Authority": {UtmStrategicCoordinationScope},
diff --git a/interfaces/openapi-to-go-server/example/api/scd/types.gen.go b/interfaces/openapi-to-go-server/example/api/scd/types.gen.go
index e14363a..2c1813f 100644
--- a/interfaces/openapi-to-go-server/example/api/scd/types.gen.go
+++ b/interfaces/openapi-to-go-server/example/api/scd/types.gen.go
@@ -4,12 +4,18 @@ package scd
 // String whose format matches a version-4 UUID according to RFC 4122.
 type UUIDv4Format string
 
+// String whose format matches a version-7 UUID according to RFC 9562.
+type UUIDv7Format string
+
 // Identifier for an Entity communicated through the DSS.  Formatted as a UUIDv4.
 type EntityID UUIDv4Format
 
 // A token associated with a particular UTM Entity+version created by the DSS upon creation or modification of an Entity reference and provided to the client creating or modifying the Entity reference.  The EntityOVN is stored privately by the DSS and then compared against entries in a Key provided to mutate the airspace.  The EntityOVN is also provided by the client whenever that client transmits the full information of the Entity (either via GET, or via a subscription notification).
 type EntityOVN string
 
+// Numeric version of this entity which increments upon each change in the entity, regardless of whether any field of the entity changes.  A USS with the details of this entity when it was at a particular version does not need to retrieve the details again until the version changes.
+type EntityVersion int32
+
 // Identifier for a subscription communicated through the DSS.  Formatted as a UUIDv4.
 type SubscriptionID UUIDv4Format
 
@@ -228,8 +234,7 @@ type OperationalIntentReference struct {
 
 	UssAvailability UssAvailabilityState `json:"uss_availability"`
 
-	// Numeric version of this operational intent which increments upon each change in the operational intent, regardless of whether any field of the operational intent reference changes.  A USS with the details of this operational intent when it was at a particular version does not need to retrieve the details again until the version changes.
-	Version int32 `json:"version"`
+	Version EntityVersion `json:"version"`
 
 	State OperationalIntentState `json:"state"`
 
@@ -269,6 +274,10 @@ type PutOperationalIntentReferenceParameters struct {
 
 	// If an existing subscription is not specified in `subscription_id`, and the operational intent is in the Activated, Nonconforming, or Contingent state, then this field must be populated.  When this field is populated, an implicit subscription will be created and associated with this operational intent, and will generally be deleted automatically upon the deletion of this operational intent.
 	NewSubscription *ImplicitSubscriptionParameters `json:"new_subscription,omitempty"`
+
+	// This optional field not part of the original F3548 standard API allows a USS to request a specific OVN when creating or updating an operational intent. When creating an operational intent, this enables a USS to immediately publish the operational intent details with the expected OVN. When updating an operational intent, this enables a USS to immediately make available this new version of the operational intent details if specifically requested by the remote USS. The USS must still wait for the DSS receipt to actually publish the new operational intent details. This allows USSs to obtain correct operational intent details even if the DSS takes a long time to respond and/or the USS processing  it.
+	// The requested suffix must be a UUIDv7 string containing a timestamp of the current time. If the suffix is invalid, and notably if the time is too far in the past or the future, the request will be rejected. If the suffix is valid, the DSS will set the OVN of the operational intent to be `{entityid}_{requested_ovn_suffix}`. If no suffix is set, the DSS will proceed as specified by the standard.
+	RequestedOvnSuffix *UUIDv7Format `json:"requested_ovn_suffix,omitempty"`
 }
 
 // Information necessary to create a subscription to serve a single operational intent's notification needs.
@@ -313,8 +322,7 @@ type ConstraintReference struct {
 
 	UssAvailability UssAvailabilityState `json:"uss_availability"`
 
-	// Numeric version of this constraint which increments upon each change in the constraint, regardless of whether any field of the constraint reference changes.  A USS with the details of this constraint when it was at a particular version does not need to retrieve the details again until the version changes.
-	Version int32 `json:"version"`
+	Version EntityVersion `json:"version"`
 
 	// Opaque version number of this constraint.  Populated only when the ConstraintReference is managed by the USS retrieving or providing it.  Not populated when the ConstraintReference is not managed by the USS retrieving or providing it (instead, the USS must obtain the OVN from the details retrieved from the managing USS).
 	Ovn *EntityOVN `json:"ovn,omitempty"`
diff --git a/pkg/api/auxv1/interface.gen.go b/pkg/api/auxv1/interface.gen.go
index 12c2d10..e20497a 100644
--- a/pkg/api/auxv1/interface.gen.go
+++ b/pkg/api/auxv1/interface.gen.go
@@ -7,13 +7,15 @@ import (
 )
 
 var (
-	GetVersionSecurity    = []api.AuthorizationOption{}
-	ValidateOauthSecurity = []api.AuthorizationOption{
+	DssWriteIdentificationServiceAreasScope = api.RequiredScope("dss.write.identification_service_areas")
+	DssReadIdentificationServiceAreasScope  = api.RequiredScope("dss.read.identification_service_areas")
+	GetVersionSecurity                      = []api.AuthorizationOption{}
+	ValidateOauthSecurity                   = []api.AuthorizationOption{
 		{
-			"Auth": {"dss.read.identification_service_areas"},
+			"Auth": {DssReadIdentificationServiceAreasScope},
 		},
 		{
-			"Auth": {"dss.write.identification_service_areas"},
+			"Auth": {DssWriteIdentificationServiceAreasScope},
 		},
 	}
 )
diff --git a/pkg/api/scdv1/interface.gen.go b/pkg/api/scdv1/interface.gen.go
index e03e500..84a4d90 100644
--- a/pkg/api/scdv1/interface.gen.go
+++ b/pkg/api/scdv1/interface.gen.go
@@ -7,11 +7,11 @@ import (
 )
 
 var (
+	UtmAvailabilityArbitrationScope          = api.RequiredScope("utm.availability_arbitration")
+	UtmStrategicCoordinationScope            = api.RequiredScope("utm.strategic_coordination")
 	UtmConstraintManagementScope             = api.RequiredScope("utm.constraint_management")
 	UtmConformanceMonitoringSaScope          = api.RequiredScope("utm.conformance_monitoring_sa")
-	UtmStrategicCoordinationScope            = api.RequiredScope("utm.strategic_coordination")
 	UtmConstraintProcessingScope             = api.RequiredScope("utm.constraint_processing")
-	UtmAvailabilityArbitrationScope          = api.RequiredScope("utm.availability_arbitration")
 	QueryOperationalIntentReferencesSecurity = []api.AuthorizationOption{
 		{
 			"Authority": {UtmStrategicCoordinationScope},
diff --git a/pkg/api/scdv1/types.gen.go b/pkg/api/scdv1/types.gen.go
index 048263a..b14deb2 100644
--- a/pkg/api/scdv1/types.gen.go
+++ b/pkg/api/scdv1/types.gen.go
@@ -4,12 +4,18 @@ package scdv1
 // String whose format matches a version-4 UUID according to RFC 4122.
 type UUIDv4Format string
 
+// String whose format matches a version-7 UUID according to RFC 9562.
+type UUIDv7Format string
+
 // Identifier for an Entity communicated through the DSS.  Formatted as a UUIDv4.
 type EntityID UUIDv4Format
 
 // A token associated with a particular UTM Entity+version created by the DSS upon creation or modification of an Entity reference and provided to the client creating or modifying the Entity reference.  The EntityOVN is stored privately by the DSS and then compared against entries in a Key provided to mutate the airspace.  The EntityOVN is also provided by the client whenever that client transmits the full information of the Entity (either via GET, or via a subscription notification).
 type EntityOVN string
 
+// Numeric version of this entity which increments upon each change in the entity, regardless of whether any field of the entity changes.  A USS with the details of this entity when it was at a particular version does not need to retrieve the details again until the version changes.
+type EntityVersion int32
+
 // Identifier for a subscription communicated through the DSS.  Formatted as a UUIDv4.
 type SubscriptionID UUIDv4Format
 
@@ -228,8 +234,7 @@ type OperationalIntentReference struct {
 
 	UssAvailability UssAvailabilityState `json:"uss_availability"`
 
-	// Numeric version of this operational intent which increments upon each change in the operational intent, regardless of whether any field of the operational intent reference changes.  A USS with the details of this operational intent when it was at a particular version does not need to retrieve the details again until the version changes.
-	Version int32 `json:"version"`
+	Version EntityVersion `json:"version"`
 
 	State OperationalIntentState `json:"state"`
 
@@ -269,6 +274,10 @@ type PutOperationalIntentReferenceParameters struct {
 
 	// If an existing subscription is not specified in `subscription_id`, and the operational intent is in the Activated, Nonconforming, or Contingent state, then this field must be populated.  When this field is populated, an implicit subscription will be created and associated with this operational intent, and will generally be deleted automatically upon the deletion of this operational intent.
 	NewSubscription *ImplicitSubscriptionParameters `json:"new_subscription,omitempty"`
+
+	// This optional field not part of the original F3548 standard API allows a USS to request a specific OVN when creating or updating an operational intent. When creating an operational intent, this enables a USS to immediately publish the operational intent details with the expected OVN. When updating an operational intent, this enables a USS to immediately make available this new version of the operational intent details if specifically requested by the remote USS. The USS must still wait for the DSS receipt to actually publish the new operational intent details. This allows USSs to obtain correct operational intent details even if the DSS takes a long time to respond and/or the USS processing  it.
+	// The requested suffix must be a UUIDv7 string containing a timestamp of the current time. If the suffix is invalid, and notably if the time is too far in the past or the future, the request will be rejected. If the suffix is valid, the DSS will set the OVN of the operational intent to be `{entityid}_{requested_ovn_suffix}`. If no suffix is set, the DSS will proceed as specified by the standard.
+	RequestedOvnSuffix *UUIDv7Format `json:"requested_ovn_suffix,omitempty"`
 }
 
 // Information necessary to create a subscription to serve a single operational intent's notification needs.
@@ -313,8 +322,7 @@ type ConstraintReference struct {
 
 	UssAvailability UssAvailabilityState `json:"uss_availability"`
 
-	// Numeric version of this constraint which increments upon each change in the constraint, regardless of whether any field of the constraint reference changes.  A USS with the details of this constraint when it was at a particular version does not need to retrieve the details again until the version changes.
-	Version int32 `json:"version"`
+	Version EntityVersion `json:"version"`
 
 	// Opaque version number of this constraint.  Populated only when the ConstraintReference is managed by the USS retrieving or providing it.  Not populated when the ConstraintReference is not managed by the USS retrieving or providing it (instead, the USS must obtain the OVN from the details retrieved from the managing USS).
 	Ovn *EntityOVN `json:"ovn,omitempty"`
diff --git a/pkg/rid/application/isa.go b/pkg/rid/application/isa.go
index 5abb820..dd9ad3f 100644
--- a/pkg/rid/application/isa.go
+++ b/pkg/rid/application/isa.go
@@ -38,7 +38,7 @@ func (a *app) GetISA(ctx context.Context, id dssmodels.ID) (*ridmodels.Identific
 	if err != nil {
 		return nil, stacktrace.Propagate(err, "Unable to interact with store")
 	}
-	return repo.GetISA(ctx, id)
+	return repo.GetISA(ctx, id, false)
 }
 
 // SearchISAs for ISA within the volume bounds.
@@ -64,7 +64,7 @@ func (a *app) DeleteISA(ctx context.Context, id dssmodels.ID, owner dssmodels.Ow
 	)
 	// The following will automatically retry TXN retry errors.
 	err := a.Store.Transact(ctx, func(repo repos.Repository) error {
-		old, err := repo.GetISA(ctx, id)
+		old, err := repo.GetISA(ctx, id, true)
 		switch {
 		case err != nil:
 			return stacktrace.Propagate(err, "Error getting ISA")
@@ -106,7 +106,7 @@ func (a *app) InsertISA(ctx context.Context, isa *ridmodels.IdentificationServic
 	// The following will automatically retry TXN retry errors.
 	err := a.Store.Transact(ctx, func(repo repos.Repository) error {
 		// ensure it doesn't exist yet
-		old, err := repo.GetISA(ctx, isa.ID)
+		old, err := repo.GetISA(ctx, isa.ID, false)
 		if err != nil {
 			return stacktrace.Propagate(err, "Error getting ISA")
 		}
@@ -141,7 +141,7 @@ func (a *app) UpdateISA(ctx context.Context, isa *ridmodels.IdentificationServic
 	err := a.Store.Transact(ctx, func(repo repos.Repository) error {
 		var err error
 
-		old, err := repo.GetISA(ctx, isa.ID)
+		old, err := repo.GetISA(ctx, isa.ID, true)
 		switch {
 		case err != nil:
 			return stacktrace.Propagate(err, "Error getting ISA")
diff --git a/pkg/rid/repos/isa.go b/pkg/rid/repos/isa.go
index 26ef202..3016f69 100644
--- a/pkg/rid/repos/isa.go
+++ b/pkg/rid/repos/isa.go
@@ -12,7 +12,7 @@ import (
 // ISA is an interface to a storage layer for the ISA entity
 type ISA interface {
 	// Returns nil, nil if not found
-	GetISA(ctx context.Context, id dssmodels.ID) (*ridmodels.IdentificationServiceArea, error)
+	GetISA(ctx context.Context, id dssmodels.ID, forUpdate bool) (*ridmodels.IdentificationServiceArea, error)
 
 	// DeleteISA deletes the IdentificationServiceArea identified by "id" and owned by "owner".
 	// Returns the delete IdentificationServiceArea and all Subscriptions affected by the delete.
diff --git a/pkg/rid/store/cockroach/identification_service_area.go b/pkg/rid/store/cockroach/identification_service_area.go
index ba6fc1c..e5b7ff6 100644
--- a/pkg/rid/store/cockroach/identification_service_area.go
+++ b/pkg/rid/store/cockroach/identification_service_area.go
@@ -101,12 +101,13 @@ func (c *isaRepo) processOne(ctx context.Context, query string, args ...interfac
 
 // GetISA returns the isa identified by "id".
 // Returns nil, nil if not found
-func (c *isaRepo) GetISA(ctx context.Context, id dssmodels.ID) (*ridmodels.IdentificationServiceArea, error) {
+func (c *isaRepo) GetISA(ctx context.Context, id dssmodels.ID, forUpdate bool) (*ridmodels.IdentificationServiceArea, error) {
 	var query = fmt.Sprintf(`
 		SELECT %s FROM
 			identification_service_areas
 		WHERE
-			id = $1`, isaFields)
+			id = $1
+        %s`, isaFields, dssql.ForUpdate(forUpdate))
 	uid, err := id.PgUUID()
 	if err != nil {
 		return nil, stacktrace.Propagate(err, "Failed to convert id to PgUUID")
diff --git a/pkg/rid/store/cockroach/identification_service_area_v3.go b/pkg/rid/store/cockroach/identification_service_area_v3.go
index 96d9c3c..121d185 100644
--- a/pkg/rid/store/cockroach/identification_service_area_v3.go
+++ b/pkg/rid/store/cockroach/identification_service_area_v3.go
@@ -82,12 +82,13 @@ func (c *isaRepoV3) processOne(ctx context.Context, query string, args ...interf
 
 // GetISA returns the isa identified by "id".
 // Returns nil, nil if not found
-func (c *isaRepoV3) GetISA(ctx context.Context, id dssmodels.ID) (*ridmodels.IdentificationServiceArea, error) {
+func (c *isaRepoV3) GetISA(ctx context.Context, id dssmodels.ID, forUpdate bool) (*ridmodels.IdentificationServiceArea, error) {
 	var query = fmt.Sprintf(`
 		SELECT %s FROM
 			identification_service_areas
 		WHERE
-			id = $1`, isaFieldsV3)
+			id = $1
+        %s`, isaFieldsV3, dssql.ForUpdate(forUpdate))
 	uid, err := id.PgUUID()
 	if err != nil {
 		return nil, stacktrace.Propagate(err, "Failed to convert id to PgUUID")
diff --git a/pkg/scd/models/constraints.go b/pkg/scd/models/constraints.go
index 7f9ae7e..4dae5e8 100644
--- a/pkg/scd/models/constraints.go
+++ b/pkg/scd/models/constraints.go
@@ -32,7 +32,7 @@ func (c *Constraint) ToRest() *restapi.ConstraintReference {
 		Id:              restapi.EntityID(c.ID.String()),
 		Ovn:             &ovn,
 		Manager:         c.Manager.String(),
-		Version:         int32(c.Version),
+		Version:         restapi.EntityVersion(c.Version),
 		UssBaseUrl:      restapi.ConstraintUssBaseURL(c.USSBaseURL),
 		UssAvailability: UssAvailabilityStateUnknown.ToRest(),
 	}
diff --git a/pkg/scd/models/operational_intents.go b/pkg/scd/models/operational_intents.go
index 9cf5c30..019e2b7 100644
--- a/pkg/scd/models/operational_intents.go
+++ b/pkg/scd/models/operational_intents.go
@@ -103,7 +103,7 @@ func (o *OperationalIntent) ToRest() *restapi.OperationalIntentReference {
 		Id:              restapi.EntityID(o.ID.String()),
 		Ovn:             &ovn,
 		Manager:         o.Manager.String(),
-		Version:         int32(o.Version),
+		Version:         restapi.EntityVersion(o.Version),
 		UssBaseUrl:      restapi.OperationalIntentUssBaseURL(o.USSBaseURL),
 		SubscriptionId:  subID,
 		State:           o.State.ToRest(),
diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go
index 86b8732..b796229 100644
--- a/pkg/scd/operational_intents_handler.go
+++ b/pkg/scd/operational_intents_handler.go
@@ -99,6 +99,13 @@ func (a *Server) DeleteOperationalIntentReference(ctx context.Context, req *rest
 				"Current version is %s but client specified version %s", old.OVN, ovn)
 		}
 
+		// Early lock on the subscriptions covering the cells relevant to the OIR
+		// See issue #1002 for details.
+		err = r.LockSubscriptionsOnCells(ctx, old.Cells)
+		if err != nil {
+			return stacktrace.Propagate(err, "Unable to acquire lock")
+		}
+
 		// Get the Subscription supporting the OperationalIntent, if one is defined
 		var previousSubscription *scdmodels.Subscription
 		if old.SubscriptionID != nil {
diff --git a/pkg/scd/store/cockroach/store.go b/pkg/scd/store/cockroach/store.go
index e4706d6..4b019de 100644
--- a/pkg/scd/store/cockroach/store.go
+++ b/pkg/scd/store/cockroach/store.go
@@ -2,6 +2,7 @@ package cockroach
 
 import (
 	"context"
+
 	"github.com/cockroachdb/cockroach-go/v2/crdb"
 	"github.com/cockroachdb/cockroach-go/v2/crdb/crdbpgxv5"
 	"github.com/coreos/go-semver/semver"
@@ -12,7 +13,6 @@ import (
 	"github.com/interuss/stacktrace"
 	"github.com/jackc/pgx/v5"
 	"github.com/jonboulle/clockwork"
-	"go.uber.org/zap"
 )
 
 const (
@@ -31,25 +31,22 @@ var (
 // repo is an implementation of repos.Repo using
 // a CockroachDB transaction.
 type repo struct {
-	q      dsssql.Queryable
-	logger *zap.Logger
-	clock  clockwork.Clock
+	q     dsssql.Queryable
+	clock clockwork.Clock
 }
 
 // Store is an implementation of an scd.Store using
 // a CockroachDB database.
 type Store struct {
-	db     *cockroach.DB
-	logger *zap.Logger
-	clock  clockwork.Clock
+	db    *cockroach.DB
+	clock clockwork.Clock
 }
 
 // NewStore returns a Store instance connected to a cockroach instance via db.
-func NewStore(ctx context.Context, db *cockroach.DB, logger *zap.Logger) (*Store, error) {
+func NewStore(ctx context.Context, db *cockroach.DB) (*Store, error) {
 	store := &Store{
-		db:     db,
-		logger: logger,
-		clock:  DefaultClock,
+		db:    db,
+		clock: DefaultClock,
 	}
 
 	if err := store.CheckCurrentMajorSchemaVersion(ctx); err != nil {
@@ -79,9 +76,8 @@ func (s *Store) CheckCurrentMajorSchemaVersion(ctx context.Context) error {
 // Interact implements store.Interactor interface.
 func (s *Store) Interact(_ context.Context) (repos.Repository, error) {
 	return &repo{
-		q:      s.db.Pool,
-		logger: s.logger,
-		clock:  s.clock,
+		q:     s.db.Pool,
+		clock: s.clock,
 	}, nil
 }
 
@@ -90,9 +86,8 @@ func (s *Store) Transact(ctx context.Context, f func(context.Context, repos.Repo
 	ctx = crdb.WithMaxRetries(ctx, flags.ConnectParameters().MaxRetries)
 	return crdbpgx.ExecuteTx(ctx, s.db.Pool, pgx.TxOptions{}, func(tx pgx.Tx) error {
 		return f(ctx, &repo{
-			q:      tx,
-			logger: s.logger,
-			clock:  s.clock,
+			q:     tx,
+			clock: s.clock,
 		})
 	})
 }
diff --git a/pkg/sql/utils.go b/pkg/sql/utils.go
index 4201076..f026174 100644
--- a/pkg/sql/utils.go
+++ b/pkg/sql/utils.go
@@ -26,3 +26,10 @@ func CellUnionToCellIdsWithValidation(cu s2.CellUnion) ([]int64, error) {
 	}
 	return pgCids, nil
 }
+
+func ForUpdate(forUpdate bool) string {
+	if forUpdate {
+		return "FOR UPDATE"
+	}
+	return ""
+}


Test patch:
diff --git a/pkg/rid/application/isa_test.go b/pkg/rid/application/isa_test.go
index 2cf54f4..155162c 100644
--- a/pkg/rid/application/isa_test.go
+++ b/pkg/rid/application/isa_test.go
@@ -31,7 +31,7 @@ type isaStore struct {
 	isas map[dssmodels.ID]*ridmodels.IdentificationServiceArea
 }
 
-func (store *isaStore) GetISA(ctx context.Context, id dssmodels.ID) (*ridmodels.IdentificationServiceArea, error) {
+func (store *isaStore) GetISA(ctx context.Context, id dssmodels.ID, forUpdate bool) (*ridmodels.IdentificationServiceArea, error) {
 	if isa, ok := store.isas[id]; ok {
 		return isa, nil
 	}
diff --git a/pkg/rid/store/cockroach/garbage_collector_test.go b/pkg/rid/store/cockroach/garbage_collector_test.go
index f525e82..02452ca 100644
--- a/pkg/rid/store/cockroach/garbage_collector_test.go
+++ b/pkg/rid/store/cockroach/garbage_collector_test.go
@@ -42,7 +42,7 @@ func TestDeleteExpiredISAs(t *testing.T) {
 	require.NoError(t, err)
 	require.NotNil(t, saOut)
 
-	ret, err := repo.GetISA(ctx, serviceArea.ID)
+	ret, err := repo.GetISA(ctx, serviceArea.ID, false)
 	require.NoError(t, err)
 	require.NotNil(t, ret)
 
@@ -50,7 +50,7 @@ func TestDeleteExpiredISAs(t *testing.T) {
 	err = gc.DeleteRIDExpiredRecords(ctx)
 	require.NoError(t, err)
 
-	ret, err = repo.GetISA(ctx, serviceArea.ID)
+	ret, err = repo.GetISA(ctx, serviceArea.ID, false)
 	require.NoError(t, err)
 	require.Nil(t, ret)
 }
diff --git a/pkg/rid/store/cockroach/identification_service_area_test.go b/pkg/rid/store/cockroach/identification_service_area_test.go
index 3196441..703b151 100644
--- a/pkg/rid/store/cockroach/identification_service_area_test.go
+++ b/pkg/rid/store/cockroach/identification_service_area_test.go
@@ -186,7 +186,7 @@ func TestStoreExpiredISA(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, serviceAreas, 1)
 
-	ret, err := repo.GetISA(ctx, serviceArea.ID)
+	ret, err := repo.GetISA(ctx, serviceArea.ID, false)
 	require.NoError(t, err)
 	require.NotNil(t, ret)
 
@@ -199,7 +199,7 @@ func TestStoreExpiredISA(t *testing.T) {
 	require.Len(t, serviceAreas, 0)
 
 	// A get should work even if it is expired.
-	ret, err = repo.GetISA(ctx, serviceArea.ID)
+	ret, err = repo.GetISA(ctx, serviceArea.ID, false)
 	require.NoError(t, err)
 	require.NotNil(t, ret)
 }
@@ -222,7 +222,7 @@ func TestStoreDeleteISAs(t *testing.T) {
 
 	// Delete the ISA.
 	// Ensure a fresh Get, then delete still updates the sub indexes
-	isa, err = repo.GetISA(ctx, isa.ID)
+	isa, err = repo.GetISA(ctx, isa.ID, false)
 	require.NoError(t, err)
 
 	serviceAreaOut, err := repo.DeleteISA(ctx, isa)
diff --git a/pkg/rid/store/cockroach/store_test.go b/pkg/rid/store/cockroach/store_test.go
index 01a76d0..08d235f 100644
--- a/pkg/rid/store/cockroach/store_test.go
+++ b/pkg/rid/store/cockroach/store_test.go
@@ -116,7 +116,7 @@ func TestTxnRetrier(t *testing.T) {
 	repo, err := store.Interact(ctx)
 	require.NoError(t, err)
 
-	isa, err := repo.GetISA(ctx, serviceArea.ID)
+	isa, err := repo.GetISA(ctx, serviceArea.ID, false)
 	require.NoError(t, err)
 	require.NotNil(t, isa)