VOOZH about

URL: https://deepwiki.com/hypervel/bus/8.1-unique-jobs

⇱ Unique Jobs | hypervel/bus | DeepWiki


Loading...
Menu

Unique Jobs

This document explains the job uniqueness system in hypervel/bus, which prevents duplicate job dispatches using distributed cache locks. The uniqueness constraint ensures that only one instance of a specific job can be dispatched within a defined time window, preventing race conditions and duplicate processing.

For information about configuring general job dispatch options, see Configuring Job Dispatch. For details about the underlying cache system, refer to the hypervel/cache documentation.

Overview

The hypervel/bus package provides a mechanism to ensure that only one instance of a job can be queued or executing at any given time. This is particularly useful for operations that should not be duplicated, such as:

  • Processing payment transactions
  • Sending notification emails
  • Updating external API resources
  • Performing data synchronization tasks

Jobs become unique by implementing the ShouldBeUnique interface from the hypervel/queue package. The system uses cache-based distributed locks to enforce uniqueness across multiple application instances.

Sources: src/PendingDispatch.php133-144 src/UniqueLock.php1-58

The ShouldBeUnique Interface

The ShouldBeUnique interface is a marker interface from the Hypervel\Queue\Contracts namespace that signals a job should be treated as unique. When a job implements this interface, the PendingDispatch wrapper checks for uniqueness before dispatching.


Diagram: ShouldBeUnique Interface Check Flow

The interface itself contains no methods—it serves purely as a type marker. When PendingDispatch.__destruct() is called, it invokes shouldDispatch() which performs the instanceof check against ShouldBeUnique.

Sources: src/PendingDispatch.php133-144

Uniqueness Configuration Methods

Jobs can customize their uniqueness behavior by implementing three optional methods or properties:

Method/PropertyTypePurposeDefault
uniqueId() or $uniqueIdstringDefines the unique identifier for the job instance'' (empty string)
uniqueFor() or $uniqueForintDuration in seconds the lock should be held0 (no expiration)
uniqueVia()CacheFactoryCustom cache instance for lock storageSystem default cache

uniqueId

The uniqueId determines what makes this specific job instance unique. The lock key is generated as:

laravel_unique_job:{JobClassName}:{uniqueId}

For example, if processing a payment for user ID 123, the uniqueId() method might return 'user-123', resulting in a lock key like:

laravel_unique_job:App\Jobs\ProcessPayment:user-123

This allows different users' payments to be processed simultaneously while preventing duplicate processing for the same user.

Sources: src/UniqueLock.php50-57

uniqueFor

The uniqueFor value specifies how long the lock should be held in seconds. After this duration, another instance of the job can be dispatched. A value of 0 means the lock never expires automatically and must be explicitly released.

The UniqueLock class retrieves this value by first checking for a uniqueFor() method, falling back to a $uniqueFor property.

Sources: src/UniqueLock.php24-26

uniqueVia

The uniqueVia() method allows specifying a custom cache instance for lock storage. This is useful when you want unique jobs to use a different cache backend (e.g., Redis vs. local cache) than the system default.

If not provided, the system uses the default CacheFactory injected into UniqueLock.

Sources: src/UniqueLock.php28-30 src/UniqueLock.php40-42

Uniqueness Check Flow

The uniqueness check occurs during job dispatch, specifically in the PendingDispatch lifecycle. Here's the complete flow:


Diagram: Unique Job Dispatch Flow

The key points in this flow:

  1. Lazy Evaluation: The uniqueness check occurs in __destruct() when PendingDispatch is destroyed, not when it's created
  2. Non-Unique Jobs: Jobs not implementing ShouldBeUnique bypass the lock check entirely
  3. Lock Acquisition: The UniqueLock.acquire() method attempts to get a cache lock with the specified duration
  4. Silent Skip: If the lock cannot be acquired (job is already dispatched), the dispatch is silently skipped—no exception is thrown

Sources: src/PendingDispatch.php133-144 src/PendingDispatch.php159-173

The UniqueLock Class

The UniqueLock class manages the cache-based locking mechanism for unique jobs. It provides two primary operations: acquiring locks and releasing locks.

Constructor and Dependencies

The UniqueLock constructor accepts a CacheFactory instance, which is retrieved from the DI container when PendingDispatch needs to check uniqueness.

Sources: src/UniqueLock.php14-17 src/PendingDispatch.php139-140

Lock Acquisition


Diagram: Lock Acquisition Process

The acquire() method performs the following steps:

  1. Determine Lock Duration: Checks for uniqueFor() method, falls back to $uniqueFor property, defaults to 0
  2. Determine Cache Instance: Checks for uniqueVia() method, falls back to injected CacheFactory
  3. Generate Lock Key: Calls getKey() to create the unique lock identifier
  4. Acquire Lock: Uses the cache's lock mechanism with lock($key, $duration)->get()
  5. Return Result: Returns true if lock was acquired, false otherwise

Sources: src/UniqueLock.php22-33

Lock Key Generation

The lock key format is:

laravel_unique_job:{FullyQualifiedClassName}:{uniqueId}

The getKey() method:

  1. Retrieves the uniqueId via method or property (defaults to empty string)
  2. Gets the fully qualified class name via get_class($job)
  3. Concatenates with the laravel_unique_job: prefix

Sources: src/UniqueLock.php50-57

Lock Release

The release() method forcibly removes a lock, allowing the job to be dispatched again:


This method is typically called after a job completes successfully, though it's not automatically invoked by the PendingDispatch class. Job implementations or queue workers must handle lock release explicitly if needed.

Sources: src/UniqueLock.php38-45

Integration with PendingDispatch

The uniqueness check is integrated into the PendingDispatch wrapper through the shouldDispatch() protected method, which is called from __destruct() before dispatching the job.

The shouldDispatch Method


This method:

  1. Type Check: Returns true immediately if job doesn't implement ShouldBeUnique
  2. Lazy DI Resolution: Retrieves CacheFactory from the DI container only when needed
  3. Lock Acquisition: Creates a new UniqueLock instance and attempts to acquire the lock
  4. Boolean Result: Returns the acquisition result, which determines if dispatch proceeds

Sources: src/PendingDispatch.php133-144

Dispatch Lifecycle


Diagram: PendingDispatch Lifecycle with Uniqueness Check

The dispatch only occurs if shouldDispatch() returns true. When a unique job's lock cannot be acquired, the job is silently skipped—no exception is thrown and no error is logged by the PendingDispatch class itself.

Sources: src/PendingDispatch.php159-173

Lock Key Namespace

The lock keys use the laravel_unique_job: prefix for historical compatibility with Laravel ecosystem tooling. This namespace ensures that unique job locks are isolated from other cache keys in the application.

The full key structure:

laravel_unique_job:{FullClassName}:{CustomUniqueId}

For example:

  • Job class: App\Jobs\ProcessPayment
  • Unique ID: payment-123
  • Lock key: laravel_unique_job:App\Jobs\ProcessPayment:payment-123

This structure allows:

  • Class-level uniqueness: When uniqueId is empty, only one instance of the entire job class can run
  • Instance-level uniqueness: When uniqueId is provided, different instances (by ID) can run concurrently
  • Tooling compatibility: Cache inspection tools can filter by the laravel_unique_job: prefix

Sources: src/UniqueLock.php56

Practical Implementation Pattern

Here's how the components work together for a typical unique job implementation:


Diagram: Complete Unique Job System Flow

Sources: src/PendingDispatch.php1-174 src/UniqueLock.php1-58

Lock Duration and Expiration

The uniqueFor value (in seconds) determines how long a job remains "unique" in the cache:

ValueBehaviorUse Case
0Lock never auto-expiresJobs that explicitly release locks after completion
60Lock expires after 60 secondsShort-running jobs with automatic cleanup
3600Lock expires after 1 hourLong-running jobs with time-based deduplication

Important: The UniqueLock class does not automatically release locks after job completion. If a job fails or times out without releasing its lock, the lock will persist until:

  1. The uniqueFor duration expires (if > 0)
  2. The lock is manually released via UniqueLock::release()
  3. The cache is cleared or the lock key is manually deleted

For jobs that should allow retry after completion, set an appropriate uniqueFor value. For jobs that should remain unique indefinitely until explicitly released, use 0 and implement lock release logic in the job's success/failure handlers.

Sources: src/UniqueLock.php24-26 src/UniqueLock.php32