Skip to content

Commit 6206bb8

Browse files
committed
Lock names include attributes values
1 parent 99495f7 commit 6206bb8

File tree

6 files changed

+235
-164
lines changed

6 files changed

+235
-164
lines changed

backend/infrahub/core/node/create.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from infrahub.core.constraint.node.runner import NodeConstraintRunner
88
from infrahub.core.manager import NodeManager
99
from infrahub.core.node import Node
10+
from infrahub.core.node.save import run_constraints_and_save
1011
from infrahub.core.protocols import CoreObjectTemplate
1112
from infrahub.dependencies.registry import get_component_registry
1213

@@ -151,8 +152,15 @@ async def _do_create_node(
151152
) -> Node:
152153
obj = await node_class.init(db=db, schema=schema, branch=branch)
153154
await obj.new(db=db, **data)
154-
await node_constraint_runner.check(node=obj, field_filters=fields_to_validate)
155-
await obj.save(db=db)
155+
156+
await run_constraints_and_save(
157+
node=obj,
158+
node_constraint_runner=node_constraint_runner,
159+
fields_to_validate=fields_to_validate,
160+
fields_to_save=[],
161+
db=db,
162+
branch=branch,
163+
)
156164

157165
object_template = await obj.get_object_template(db=db)
158166
if object_template:

backend/infrahub/core/node/save.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from infrahub import lock
2+
from infrahub.core.branch import Branch
3+
from infrahub.core.constraint.node.runner import NodeConstraintRunner
4+
from infrahub.core.node import Node
5+
from infrahub.database import InfrahubDatabase
6+
from infrahub.lock import InfrahubMultiLock
7+
from infrahub.lock_utils import _get_lock_names_on_object_mutation
8+
9+
10+
async def run_constraints_and_save(
11+
node: Node,
12+
node_constraint_runner: NodeConstraintRunner,
13+
fields_to_validate: list[str],
14+
fields_to_save: list[str],
15+
db: InfrahubDatabase,
16+
branch: Branch,
17+
skip_uniqueness_check: bool = False,
18+
) -> None:
19+
schema_branch = db.schema.get_schema_branch(name=branch.name)
20+
lock_names = _get_lock_names_on_object_mutation(node=node, branch=branch, schema_branch=schema_branch)
21+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
22+
await node_constraint_runner.check(
23+
node=node, field_filters=fields_to_validate, skip_uniqueness_check=skip_uniqueness_check
24+
)
25+
await node.save(db=db, fields=fields_to_save)

backend/infrahub/graphql/mutations/ipam.py

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,16 @@ async def _mutate_create_object_and_reconcile(
118118
) -> Node:
119119
address = await cls.mutate_create_object(data=data, db=db, branch=branch)
120120
reconciler = IpamReconciler(db=db, branch=branch)
121-
reconciled_address = await reconciler.reconcile(
122-
ip_value=ip_address, namespace=namespace_id, node_uuid=address.get_id()
123-
)
121+
122+
if lock_name := cls._get_lock_name(namespace_id, branch):
123+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
124+
reconciled_address = await reconciler.reconcile(
125+
ip_value=ip_address, namespace=namespace_id, node_uuid=address.get_id()
126+
)
127+
else:
128+
reconciled_address = await reconciler.reconcile(
129+
ip_value=ip_address, namespace=namespace_id, node_uuid=address.get_id()
130+
)
124131
return reconciled_address
125132

126133
@classmethod
@@ -138,15 +145,9 @@ async def mutate_create(
138145
namespace_id = await validate_namespace(db=db, branch=branch, data=data)
139146

140147
async with db.start_transaction() as dbt:
141-
if lock_name := cls._get_lock_name(namespace_id, branch):
142-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
143-
reconciled_address = await cls._mutate_create_object_and_reconcile(
144-
data=data, branch=branch, db=dbt, ip_address=ip_address, namespace_id=namespace_id
145-
)
146-
else:
147-
reconciled_address = await cls._mutate_create_object_and_reconcile(
148-
data=data, branch=branch, db=dbt, ip_address=ip_address, namespace_id=namespace_id
149-
)
148+
reconciled_address = await cls._mutate_create_object_and_reconcile(
149+
data=data, branch=branch, db=dbt, ip_address=ip_address, namespace_id=namespace_id
150+
)
150151
result = await cls.mutate_create_to_graphql(info=info, db=dbt, obj=reconciled_address)
151152

152153
return reconciled_address, result
@@ -164,9 +165,15 @@ async def _mutate_update_object_and_reconcile(
164165
address = await cls.mutate_update_object(db=db, info=info, data=data, branch=branch, obj=address)
165166
reconciler = IpamReconciler(db=db, branch=branch)
166167
ip_address = ipaddress.ip_interface(address.address.value)
167-
reconciled_address = await reconciler.reconcile(
168-
ip_value=ip_address, node_uuid=address.get_id(), namespace=namespace_id
169-
)
168+
if lock_name := cls._get_lock_name(namespace_id, branch):
169+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
170+
reconciled_address = await reconciler.reconcile(
171+
ip_value=ip_address, node_uuid=address.get_id(), namespace=namespace_id
172+
)
173+
else:
174+
reconciled_address = await reconciler.reconcile(
175+
ip_value=ip_address, node_uuid=address.get_id(), namespace=namespace_id
176+
)
170177
return reconciled_address
171178

172179
@classmethod
@@ -194,16 +201,9 @@ async def mutate_update(
194201
namespace_id = await validate_namespace(db=db, branch=branch, data=data, existing_namespace_id=namespace.id)
195202

196203
async with db.start_transaction() as dbt:
197-
if lock_name := cls._get_lock_name(namespace_id, branch):
198-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
199-
reconciled_address = await cls._mutate_update_object_and_reconcile(
200-
info=info, data=data, branch=branch, address=address, namespace_id=namespace_id, db=dbt
201-
)
202-
else:
203-
reconciled_address = await cls._mutate_update_object_and_reconcile(
204-
info=info, data=data, branch=branch, address=address, namespace_id=namespace_id, db=dbt
205-
)
206-
204+
reconciled_address = await cls._mutate_update_object_and_reconcile(
205+
info=info, data=data, branch=branch, address=address, namespace_id=namespace_id, db=dbt
206+
)
207207
result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=reconciled_address)
208208

209209
return address, result
@@ -289,12 +289,9 @@ async def mutate_create(
289289
namespace_id = await validate_namespace(db=db, branch=branch, data=data)
290290

291291
async with db.start_transaction() as dbt:
292-
lock_name = cls._get_lock_name(namespace_id)
293-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
294-
reconciled_prefix = await cls._mutate_create_object_and_reconcile(
295-
data=data, branch=branch, db=dbt, namespace_id=namespace_id
296-
)
297-
292+
reconciled_prefix = await cls._mutate_create_object_and_reconcile(
293+
data=data, branch=branch, db=dbt, namespace_id=namespace_id
294+
)
298295
result = await cls.mutate_create_to_graphql(info=info, db=dbt, obj=reconciled_prefix)
299296

300297
return reconciled_prefix, result
@@ -339,11 +336,9 @@ async def mutate_update(
339336
namespace_id = await validate_namespace(db=db, branch=branch, data=data, existing_namespace_id=namespace.id)
340337

341338
async with db.start_transaction() as dbt:
342-
lock_name = cls._get_lock_name(namespace_id)
343-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
344-
reconciled_prefix = await cls._mutate_update_object_and_reconcile(
345-
info=info, data=data, prefix=prefix, db=dbt, namespace_id=namespace_id, branch=branch
346-
)
339+
reconciled_prefix = await cls._mutate_update_object_and_reconcile(
340+
info=info, data=data, prefix=prefix, db=dbt, namespace_id=namespace_id, branch=branch
341+
)
347342
result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=reconciled_prefix)
348343

349344
return prefix, result
@@ -378,9 +373,15 @@ async def _reconcile_prefix(
378373
) -> Node:
379374
reconciler = IpamReconciler(db=db, branch=branch)
380375
ip_network = ipaddress.ip_network(prefix.prefix.value)
381-
reconciled_prefix = await reconciler.reconcile(
382-
ip_value=ip_network, node_uuid=prefix.get_id(), namespace=namespace_id, is_delete=is_delete
383-
)
376+
if lock_name := cls._get_lock_name(namespace_id):
377+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
378+
reconciled_prefix = await reconciler.reconcile(
379+
ip_value=ip_network, node_uuid=prefix.get_id(), namespace=namespace_id, is_delete=is_delete
380+
)
381+
else:
382+
reconciled_prefix = await reconciler.reconcile(
383+
ip_value=ip_network, node_uuid=prefix.get_id(), namespace=namespace_id, is_delete=is_delete
384+
)
384385
return reconciled_prefix
385386

386387
@classmethod
@@ -404,11 +405,9 @@ async def mutate_delete(
404405
namespace_id = namespace_rels[0].peer_id
405406

406407
async with graphql_context.db.start_transaction() as dbt:
407-
lock_name = cls._get_lock_name(namespace_id)
408-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=[lock_name]):
409-
reconciled_prefix = await cls._reconcile_prefix(
410-
branch=branch, db=dbt, prefix=prefix, namespace_id=namespace_id, is_delete=True
411-
)
408+
reconciled_prefix = await cls._reconcile_prefix(
409+
branch=branch, db=dbt, prefix=prefix, namespace_id=namespace_id, is_delete=True
410+
)
412411
ok = True
413412

414413
return DeleteResult(node=reconciled_prefix, mutation=cls(ok=ok))

backend/infrahub/graphql/mutations/main.py

Lines changed: 24 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from infrahub_sdk.utils import extract_fields
99
from typing_extensions import Self
1010

11-
from infrahub import config, lock
12-
from infrahub.core.constants import InfrahubKind, MutationAction
11+
from infrahub import config
12+
from infrahub.core.constants import MutationAction
1313
from infrahub.core.constraint.node.runner import NodeConstraintRunner
1414
from infrahub.core.manager import NodeManager
1515
from infrahub.core.node.create import (
@@ -27,17 +27,16 @@
2727
from infrahub.events.generator import generate_node_mutation_events
2828
from infrahub.exceptions import HFIDViolatedError, InitializationError, NodeNotFoundError
2929
from infrahub.graphql.context import apply_external_context
30-
from infrahub.lock import InfrahubMultiLock, build_object_lock_name
3130
from infrahub.log import get_log_data, get_logger
3231

32+
from ...core.node.save import run_constraints_and_save
3333
from .node_getter.by_default_filter import MutationNodeGetterByDefaultFilter
3434

3535
if TYPE_CHECKING:
3636
from graphql import GraphQLResolveInfo
3737

3838
from infrahub.core.branch import Branch
3939
from infrahub.core.node import Node
40-
from infrahub.core.schema.schema_branch import SchemaBranch
4140
from infrahub.database import InfrahubDatabase
4241
from infrahub.graphql.types.context import ContextInput
4342

@@ -46,8 +45,6 @@
4645

4746
log = get_logger()
4847

49-
KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [InfrahubKind.GENERICGROUP]
50-
5148

5249
@dataclass
5350
class DeleteResult:
@@ -150,14 +147,6 @@ async def _call_mutate_create_object(cls, data: InputObjectType, db: InfrahubDat
150147
"""
151148
Wrapper around mutate_create_object to potentially activate locking.
152149
"""
153-
schema_branch = db.schema.get_schema_branch(name=branch.name)
154-
lock_names = _get_kind_lock_names_on_object_mutation(
155-
kind=cls._meta.active_schema.kind, branch=branch, schema_branch=schema_branch
156-
)
157-
if lock_names:
158-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
159-
return await cls.mutate_create_object(data=data, db=db, branch=branch)
160-
161150
return await cls.mutate_create_object(data=data, db=db, branch=branch)
162151

163152
@classmethod
@@ -214,39 +203,22 @@ async def _call_mutate_update(
214203
Wrapper around mutate_update to potentially activate locking and call it within a database transaction.
215204
"""
216205

217-
schema_branch = db.schema.get_schema_branch(name=branch.name)
218-
lock_names = _get_kind_lock_names_on_object_mutation(
219-
kind=cls._meta.active_schema.kind, branch=branch, schema_branch=schema_branch
220-
)
221-
222206
if db.is_transaction:
223-
if lock_names:
224-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
225-
obj = await cls.mutate_update_object(
226-
db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
227-
)
228-
else:
229-
obj = await cls.mutate_update_object(
230-
db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
231-
)
207+
obj = await cls.mutate_update_object(
208+
db=db, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
209+
)
232210
result = await cls.mutate_update_to_graphql(db=db, info=info, obj=obj)
233211
return obj, result
234212

235213
async with db.start_transaction() as dbt:
236-
if lock_names:
237-
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
238-
obj = await cls.mutate_update_object(
239-
db=dbt,
240-
info=info,
241-
data=data,
242-
branch=branch,
243-
obj=obj,
244-
skip_uniqueness_check=skip_uniqueness_check,
245-
)
246-
else:
247-
obj = await cls.mutate_update_object(
248-
db=dbt, info=info, data=data, branch=branch, obj=obj, skip_uniqueness_check=skip_uniqueness_check
249-
)
214+
obj = await cls.mutate_update_object(
215+
db=dbt,
216+
info=info,
217+
data=data,
218+
branch=branch,
219+
obj=obj,
220+
skip_uniqueness_check=skip_uniqueness_check,
221+
)
250222
result = await cls.mutate_update_to_graphql(db=dbt, info=info, obj=obj)
251223
return obj, result
252224

@@ -287,16 +259,21 @@ async def mutate_update_object(
287259
before_mutate_profile_ids = await get_profile_ids(db=db, obj=obj)
288260
await obj.from_graphql(db=db, data=data)
289261
fields_to_validate = list(data)
290-
await node_constraint_runner.check(
291-
node=obj, field_filters=fields_to_validate, skip_uniqueness_check=skip_uniqueness_check
292-
)
293-
294262
fields = list(data.keys())
263+
295264
for field_to_remove in ("id", "hfid"):
296265
if field_to_remove in fields:
297266
fields.remove(field_to_remove)
298267

299-
await obj.save(db=db, fields=fields)
268+
await run_constraints_and_save(
269+
node=obj,
270+
node_constraint_runner=node_constraint_runner,
271+
fields_to_validate=fields_to_validate,
272+
fields_to_save=fields,
273+
db=db,
274+
skip_uniqueness_check=skip_uniqueness_check,
275+
branch=branch,
276+
)
300277

301278
obj = await refresh_for_profile_update(
302279
db=db,
@@ -465,71 +442,5 @@ def __init_subclass_with_meta__(
465442
super().__init_subclass_with_meta__(_meta=_meta, **options)
466443

467444

468-
def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
469-
"""
470-
Return kinds for which we want to lock during creating / updating an object of a given schema node.
471-
Lock should be performed on schema kind and its generics having a uniqueness_constraint defined.
472-
If a generic uniqueness constraint is the same as the node schema one,
473-
it means node schema overrided this constraint, in which case we only need to lock on the generic.
474-
"""
475-
476-
node_schema = schema_branch.get(name=kind)
477-
478-
schema_uc = None
479-
kinds = []
480-
if node_schema.uniqueness_constraints:
481-
kinds.append(node_schema.kind)
482-
schema_uc = node_schema.uniqueness_constraints
483-
484-
if node_schema.is_generic_schema:
485-
return kinds
486-
487-
generics_kinds = node_schema.inherit_from
488-
489-
node_schema_kind_removed = False
490-
for generic_kind in generics_kinds:
491-
generic_uc = schema_branch.get(name=generic_kind).uniqueness_constraints
492-
if generic_uc:
493-
kinds.append(generic_kind)
494-
if not node_schema_kind_removed and generic_uc == schema_uc:
495-
# Check whether we should remove original schema kind as it simply overrides uniqueness_constraint
496-
# of a generic
497-
kinds.pop(0)
498-
node_schema_kind_removed = True
499-
return kinds
500-
501-
502-
def _should_kind_be_locked_on_any_branch(kind: str, schema_branch: SchemaBranch) -> bool:
503-
"""
504-
Check whether kind or any kind generic is in KINDS_TO_LOCK_ON_ANY_BRANCH.
505-
"""
506-
507-
if kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
508-
return True
509-
510-
node_schema = schema_branch.get(name=kind)
511-
if node_schema.is_generic_schema:
512-
return False
513-
514-
for generic_kind in node_schema.inherit_from:
515-
if generic_kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
516-
return True
517-
return False
518-
519-
520-
def _get_kind_lock_names_on_object_mutation(kind: str, branch: Branch, schema_branch: SchemaBranch) -> list[str]:
521-
"""
522-
Return objects kind for which we want to avoid concurrent mutation (create/update). Except for some specific kinds,
523-
concurrent mutations are only allowed on non-main branch as objects validations will be performed at least when merging in main branch.
524-
"""
525-
526-
if not branch.is_default and not _should_kind_be_locked_on_any_branch(kind, schema_branch):
527-
return []
528-
529-
lock_kinds = _get_kinds_to_lock_on_object_mutation(kind, schema_branch)
530-
lock_names = [build_object_lock_name(kind) for kind in lock_kinds]
531-
return lock_names
532-
533-
534445
def _get_data_fields(data: InputObjectType) -> list[str]:
535446
return [field for field in data.keys() if field not in ["id", "hfid"]]

0 commit comments

Comments
 (0)