Skip to content

Conversation

@hxnk
Copy link

@hxnk hxnk commented Oct 22, 2025

Problem

The queue system uses get_class($job) to identify jobs for:

  • Unique job locks (UniqueLock::getKey())
  • Exception throttling (ThrottlesExceptions::getKey())
  • Overlap prevention (WithoutOverlapping::getLockKey())

This doesn't support job wrapper patterns where multiple actions use the same wrapper class. This affects popular packages:

Since all actions share the same wrapper class name, they incorrectly share the same lock/cache keys.

Solution

Add an optional jobTypeIdentifier() method that jobs can implement to provide a custom identifier as an alternative to get_class().

Before:

// UniqueLock.php line 72
return 'laravel_unique_job:'.get_class($job).':'.$uniqueId;

After:

$jobType = method_exists($job, 'jobTypeIdentifier')
    ? $job->jobTypeIdentifier()
    : get_class($job);

return 'laravel_unique_job:'.$jobType.':'.$uniqueId;

Wrapper packages can then implement:

class ActionJob implements ShouldQueue
{
    public function __construct(
        protected string $actionClass,
        protected array $parameters = []
    ) {}

    public function jobTypeIdentifier(): string
    {
        return $this->actionClass; // Return wrapped action class
    }
}

Changes

Applied the same pattern to all three identification points:

  • src/Illuminate/Bus/UniqueLock.php
  • src/Illuminate/Queue/Middleware/ThrottlesExceptions.php
  • src/Illuminate/Queue/Middleware/WithoutOverlapping.php

Tests added for both default (get_class()) and custom identifier behavior.

Backward Compatibility

✅ Fully backward compatible, existing jobs continue using get_class() as before.

@jmarble
Copy link

jmarble commented Oct 24, 2025

I think your idea has merit, but I personally don't like seeing things like this for BC:

method_exists($job, 'jobTypeIdentifier')

@hxnk
Copy link
Author

hxnk commented Oct 24, 2025

Just to clarify your BC concern:

  1. You don't like method_exists() as a pattern for maintaining BC, or
  2. You're worried existing code might already have a jobTypeIdentifier() method?

Concern 1: method_exists() is already used consistently in the framework for optional methods like middleware(), failed(), uniqueId() etc.

Concern 2: While collision risk is small but not zero, an interface would avoid it entirely:

interface HasJobTypeIdentifier
{
    public function jobTypeIdentifier(): string;
}

$jobType = $job instanceof HasJobTypeIdentifier
    ? $job->jobTypeIdentifier()
    : get_class($job);

This requires explicit opt-in, which I'm fine with.

Alternatively, uniqueJobType() might be a better method name, matching other uniqueness-related methods. With or without an interface to avoid collision risk.

@taylorotwell
Copy link
Member

I think you can just use the existing $job->resolveName() method. It will fallback to the job's displayName method for wrapped things like Mailables, Notifications, etc. and I think these packages should just define a displayName method if they aren't already.

Mark as ready for review when the changes are made. 🫡

@taylorotwell taylorotwell marked this pull request as draft October 24, 2025 14:25
@hxnk hxnk changed the title Add jobTypeIdentifier() method for custom job identification [12.x] Use displayName() for custom job identification Oct 24, 2025
@hxnk
Copy link
Author

hxnk commented Oct 24, 2025

Updated to check for displayName() method on job classes.

Why displayName() instead of resolveName()

resolveName() only exists on the queue wrapper (DatabaseJob, RedisJob, etc), which isn't available in all scenarios:

  • UniqueLock::acquire() is called pre-dispatch (PendingDispatch.php:208) on the job class before it's queued
  • Middleware receives the job class/command, not the queue wrapper, and will only have access to the queue wrapper if the job uses InteractsWithQueue
  • Users might have existing jobs without InteractsWithQueue, so relying on the queue wrapper being available would be a breaking change

Proposed implementation

Before:

// UniqueLock.php line 72
return 'laravel_unique_job:'.get_class($job).':'.$uniqueId;

After:

$jobName = method_exists($job, 'displayName')
    ? $job->displayName()
    : get_class($job);

return 'laravel_unique_job:'.$jobName.':'.$uniqueId;

Applied this pattern consistently across:

  • src/Illuminate/Bus/UniqueLock.php
  • src/Illuminate/Queue/Middleware/ThrottlesExceptions.php
  • src/Illuminate/Queue/Middleware/WithoutOverlapping.php

@hxnk hxnk marked this pull request as ready for review October 24, 2025 18:17
@hxnk hxnk marked this pull request as draft October 24, 2025 18:18
@hxnk hxnk marked this pull request as ready for review October 24, 2025 18:49
@jmarble
Copy link

jmarble commented Oct 25, 2025

I think what @taylorotwell was trying to say is that it should look something like this, which would be BC and use the frameworks existing API:

return 'laravel_unique_job:'.$job->resolveName().':'.$uniqueId;

Then the package would simply implement the displayName() on their wrapper job.

@hxnk
Copy link
Author

hxnk commented Oct 25, 2025

Thank you for the suggestion. However, resolveName() exists on the queue wrapper (DatabaseJob, RedisJob, etc.) rather
than on the job class itself. To use it, the implementation would need to be:

return 'laravel_unique_job:'.$job->job->resolveName().':'.$uniqueId;

This approach presents issues where $job->job would be unavailable:

1. Pre-dispatch context
UniqueLock::acquire() is called before the job is queued (PendingDispatch.php:208). At this point, $job->job is
always null.

2. Jobs without InteractsWithQueue
The queue wrapper is only injected when a job uses the InteractsWithQueue trait (CallQueuedHandler.php:165-171). Existing jobs without this trait would have $job->job as null. Middleware receives the job class/command, not the queue wrapper.

Relying on the queue wrapper being available would break in above scenarios. The displayName() approach works universally across all contexts.

@hxnk
Copy link
Author

hxnk commented Oct 25, 2025

@taylorotwell I'd like to bring up one additional consideration regarding displayName().

The method name suggests non-technical usage, as if it's purely for visual representation of jobs. However, this implementation gives it technical implications for lock/cache key generation, which could lead to unexpected behavior. In theory, developers could assign the same displayName() to multiple different job classes. While this is valid for display purposes only (as far as I can see), it would cause lock collisions in this context where two distinct jobs share the same lock key through displayName().

A potential solution would be to concatenate both the actual class name and the displayName() in the key generation:

$jobName = method_exists($job, 'displayName')
    ? get_class($job).':'.$job->displayName()
    : get_class($job);

return 'laravel_unique_job:'.$jobName.':'.$uniqueId;

This approach would:

  1. Solve the original problem this PR addresses (packages using job wrappers can provide custom identification)
  2. Prevent lock collisions if multiple jobs happen to use the same display name
  3. Maintain backward compatibility (jobs without displayName() still use class name only)

For now, I've kept the PR using displayName() only to avoid cluttering the discussion. However, I'm happy to adjust to this concatenated approach if you think it better addresses potential edge cases. I wanted to hear your perspective first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants