1
1
from dataclasses import dataclass , field
2
2
from datetime import datetime , timezone , timedelta
3
+ from hashlib import sha256
3
4
import re
4
5
from typing import (
5
- Awaitable ,
6
6
Optional ,
7
7
Protocol ,
8
8
Tuple ,
9
9
List ,
10
- Callable ,
11
10
cast ,
12
11
runtime_checkable ,
13
12
)
@@ -206,12 +205,7 @@ def to_json(self) -> str:
206
205
insert_opts = InsertOpts ()
207
206
insert_params , unique_opts = _make_driver_insert_params (args , insert_opts )
208
207
209
- async def insert ():
210
- return InsertResult (await exec .job_insert (insert_params ))
211
-
212
- return await self .__check_unique_job (
213
- exec , insert_params , unique_opts , insert
214
- )
208
+ return await self .__insert_job_with_unique (exec , insert_params , unique_opts )
215
209
216
210
async def insert_tx (
217
211
self , tx , args : JobArgs , insert_opts : Optional [InsertOpts ] = None
@@ -253,10 +247,7 @@ async def insert_tx(
253
247
insert_opts = InsertOpts ()
254
248
insert_params , unique_opts = _make_driver_insert_params (args , insert_opts )
255
249
256
- async def insert ():
257
- return InsertResult (await exec .job_insert (insert_params ))
258
-
259
- return await self .__check_unique_job (exec , insert_params , unique_opts , insert )
250
+ return await self .__insert_job_with_unique (exec , insert_params , unique_opts )
260
251
261
252
async def insert_many (self , args : List [JobArgs | InsertManyParams ]) -> int :
262
253
"""
@@ -327,33 +318,50 @@ async def insert_many_tx(self, tx, args: List[JobArgs | InsertManyParams]) -> in
327
318
exec = self .driver .unwrap_executor (tx )
328
319
return await exec .job_insert_many (_make_driver_insert_params_many (args ))
329
320
330
- async def __check_unique_job (
321
+ async def __insert_job_with_unique (
331
322
self ,
332
323
exec : AsyncExecutorProtocol ,
333
324
insert_params : JobInsertParams ,
334
325
unique_opts : Optional [UniqueOpts ],
335
- insert_func : Callable [[], Awaitable [InsertResult ]],
336
326
) -> InsertResult :
337
327
"""
338
- Checks for an existing unique job and runs `insert_func` if one is
339
- present.
328
+ Inserts a job, accounting for unique jobs whose insertion may be skipped
329
+ if an equivalent job is already present.
340
330
"""
341
331
342
- get_params , lock_key = _build_unique_get_params_and_lock_key (
343
- self . advisory_lock_prefix , insert_params , unique_opts
332
+ get_params , unique_key = _build_unique_get_params_and_unique_key (
333
+ insert_params , unique_opts
344
334
)
345
335
346
- if not get_params :
347
- return await insert_func ()
336
+ if not get_params or not unique_opts :
337
+ return InsertResult (await exec .job_insert (insert_params ))
338
+
339
+ # fast path
340
+ if (
341
+ not unique_opts .by_state
342
+ or unique_opts .by_state .sort == UNIQUE_STATES_DEFAULT
343
+ ):
344
+ job , unique_skipped_as_duplicate = await exec .job_insert_unique (
345
+ insert_params , sha256 (unique_key .encode ("utf-8" )).digest ()
346
+ )
347
+ return InsertResult (
348
+ job = job , unique_skipped_as_duplicated = unique_skipped_as_duplicate
349
+ )
348
350
349
351
async with exec .transaction ():
350
- await exec .advisory_lock (lock_key )
352
+ lock_key = "unique_key"
353
+ lock_key += "kind=#{insert_params.kind}"
354
+ lock_key += unique_key
355
+
356
+ await exec .advisory_lock (
357
+ _hash_lock_key (self .advisory_lock_prefix , lock_key )
358
+ )
351
359
352
360
existing_job = await exec .job_get_by_kind_and_unique_properties (get_params )
353
361
if existing_job :
354
362
return InsertResult (existing_job , unique_skipped_as_duplicated = True )
355
363
356
- return await insert_func ( )
364
+ return InsertResult ( await exec . job_insert ( insert_params ) )
357
365
358
366
359
367
class Client :
@@ -451,10 +459,7 @@ def to_json(self) -> str:
451
459
insert_opts = InsertOpts ()
452
460
insert_params , unique_opts = _make_driver_insert_params (args , insert_opts )
453
461
454
- def insert ():
455
- return InsertResult (exec .job_insert (insert_params ))
456
-
457
- return self .__check_unique_job (exec , insert_params , unique_opts , insert )
462
+ return self .__insert_job_with_unique (exec , insert_params , unique_opts )
458
463
459
464
def insert_tx (
460
465
self , tx , args : JobArgs , insert_opts : Optional [InsertOpts ] = None
@@ -496,10 +501,7 @@ def insert_tx(
496
501
insert_opts = InsertOpts ()
497
502
insert_params , unique_opts = _make_driver_insert_params (args , insert_opts )
498
503
499
- def insert ():
500
- return InsertResult (exec .job_insert (insert_params ))
501
-
502
- return self .__check_unique_job (exec , insert_params , unique_opts , insert )
504
+ return self .__insert_job_with_unique (exec , insert_params , unique_opts )
503
505
504
506
def insert_many (self , args : List [JobArgs | InsertManyParams ]) -> int :
505
507
"""
@@ -570,58 +572,72 @@ def insert_many_tx(self, tx, args: List[JobArgs | InsertManyParams]) -> int:
570
572
exec = self .driver .unwrap_executor (tx )
571
573
return exec .job_insert_many (_make_driver_insert_params_many (args ))
572
574
573
- def __check_unique_job (
575
+ def __insert_job_with_unique (
574
576
self ,
575
577
exec : ExecutorProtocol ,
576
578
insert_params : JobInsertParams ,
577
579
unique_opts : Optional [UniqueOpts ],
578
- insert_func : Callable [[], InsertResult ],
579
580
) -> InsertResult :
580
581
"""
581
- Checks for an existing unique job and runs `insert_func` if one is
582
- present.
582
+ Inserts a job, accounting for unique jobs whose insertion may be skipped
583
+ if an equivalent job is already present.
583
584
"""
584
585
585
- get_params , lock_key = _build_unique_get_params_and_lock_key (
586
- self . advisory_lock_prefix , insert_params , unique_opts
586
+ get_params , unique_key = _build_unique_get_params_and_unique_key (
587
+ insert_params , unique_opts
587
588
)
588
589
589
- if not get_params :
590
- return insert_func ()
590
+ if not get_params or not unique_opts :
591
+ return InsertResult (exec .job_insert (insert_params ))
592
+
593
+ # fast path
594
+ if (
595
+ not unique_opts .by_state
596
+ or unique_opts .by_state .sort == UNIQUE_STATES_DEFAULT
597
+ ):
598
+ job , unique_skipped_as_duplicate = exec .job_insert_unique (
599
+ insert_params , sha256 (unique_key .encode ("utf-8" )).digest ()
600
+ )
601
+ return InsertResult (
602
+ job = job , unique_skipped_as_duplicated = unique_skipped_as_duplicate
603
+ )
591
604
592
605
with exec .transaction ():
593
- exec .advisory_lock (lock_key )
606
+ lock_key = "unique_key"
607
+ lock_key += "kind=#{insert_params.kind}"
608
+ lock_key += unique_key
609
+
610
+ exec .advisory_lock (_hash_lock_key (self .advisory_lock_prefix , lock_key ))
594
611
595
612
existing_job = exec .job_get_by_kind_and_unique_properties (get_params )
596
613
if existing_job :
597
614
return InsertResult (existing_job , unique_skipped_as_duplicated = True )
598
615
599
- return insert_func ( )
616
+ return InsertResult ( exec . job_insert ( insert_params ) )
600
617
601
618
602
- def _build_unique_get_params_and_lock_key (
603
- advisory_lock_prefix : Optional [int ],
619
+ def _build_unique_get_params_and_unique_key (
604
620
insert_params : JobInsertParams ,
605
621
unique_opts : Optional [UniqueOpts ],
606
- ) -> tuple [Optional [JobGetByKindAndUniquePropertiesParam ], int ]:
622
+ ) -> tuple [Optional [JobGetByKindAndUniquePropertiesParam ], str ]:
607
623
"""
608
624
Builds driver get params and an advisory lock key from insert params and
609
625
unique options for use during a unique job insertion.
610
626
"""
611
627
612
628
if unique_opts is None :
613
- return (None , 0 )
629
+ return (None , "" )
614
630
615
631
any_unique_opts = False
616
632
get_params = JobGetByKindAndUniquePropertiesParam (kind = insert_params .kind )
617
633
618
- lock_str = f"unique_keykind= { insert_params . kind } "
634
+ unique_key = " "
619
635
620
636
if unique_opts .by_args :
621
637
any_unique_opts = True
622
638
get_params .by_args = True
623
639
get_params .args = insert_params .args
624
- lock_str += f"&args={ insert_params .args } "
640
+ unique_key += f"&args={ insert_params .args } "
625
641
626
642
if unique_opts .by_period :
627
643
lower_period_bound = _truncate_time (
@@ -634,33 +650,27 @@ def _build_unique_get_params_and_lock_key(
634
650
lower_period_bound ,
635
651
lower_period_bound + timedelta (seconds = unique_opts .by_period ),
636
652
]
637
- lock_str += f"&period={ lower_period_bound .strftime ('%FT%TZ' )} "
653
+ unique_key += f"&period={ lower_period_bound .strftime ('%FT%TZ' )} "
638
654
639
655
if unique_opts .by_queue :
640
656
any_unique_opts = True
641
657
get_params .by_queue = True
642
658
get_params .queue = insert_params .queue
643
- lock_str += f"&queue={ insert_params .queue } "
659
+ unique_key += f"&queue={ insert_params .queue } "
644
660
645
661
if unique_opts .by_state :
646
662
any_unique_opts = True
647
663
get_params .by_state = True
648
664
get_params .state = cast (list [str ], unique_opts .by_state )
649
- lock_str += f"&state={ ',' .join (unique_opts .by_state )} "
665
+ unique_key += f"&state={ ',' .join (unique_opts .by_state )} "
650
666
else :
651
667
get_params .state = UNIQUE_STATES_DEFAULT
652
- lock_str += f"&state={ ',' .join (UNIQUE_STATES_DEFAULT )} "
668
+ unique_key += f"&state={ ',' .join (UNIQUE_STATES_DEFAULT )} "
653
669
654
670
if not any_unique_opts :
655
- return (None , 0 )
671
+ return (None , "" )
656
672
657
- if advisory_lock_prefix is None :
658
- lock_key = fnv1_hash (lock_str .encode ("utf-8" ), 64 )
659
- else :
660
- prefix = advisory_lock_prefix
661
- lock_key = (prefix << 32 ) | fnv1_hash (lock_str .encode ("utf-8" ), 32 )
662
-
663
- return (get_params , _uint64_to_int64 (lock_key ))
673
+ return (get_params , unique_key )
664
674
665
675
666
676
def _check_advisory_lock_prefix_bounds (
@@ -678,6 +688,21 @@ def _check_advisory_lock_prefix_bounds(
678
688
return advisory_lock_prefix
679
689
680
690
691
+ def _hash_lock_key (advisory_lock_prefix : Optional [int ], lock_key : str ) -> int :
692
+ """
693
+ Generates an FNV-1 hash from the given lock key string suitable for use with
694
+ a PG advisory lock while checking for the existence of a unique job.
695
+ """
696
+
697
+ if advisory_lock_prefix is None :
698
+ lock_key_hash = fnv1_hash (lock_key .encode ("utf-8" ), 64 )
699
+ else :
700
+ prefix = advisory_lock_prefix
701
+ lock_key_hash = (prefix << 32 ) | fnv1_hash (lock_key .encode ("utf-8" ), 32 )
702
+
703
+ return _uint64_to_int64 (lock_key_hash )
704
+
705
+
681
706
def _make_driver_insert_params (
682
707
args : JobArgs ,
683
708
insert_opts : InsertOpts ,
0 commit comments