Implement stateful jobs in the job queue infrastructure
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Objective
Add support for stateful jobs in our job queue system, allowing jobs to be marked as "stateful" to ensure only one job of the same type (queue name) runs at a time.
Background
In our migration from Quartz to our custom job queue system, we need to support the concept of "stateful jobs" where only one job of a specific type can run at a time. This is similar to Quartz's stateful job behavior, which prevents concurrent execution of jobs with the same identity.
Implementation Details
1. Database Schema Updates
Add a stateful flag to the job_queue table:
-- Add stateful column to job_queue table
ALTER TABLE job_queue ADD COLUMN stateful BOOLEAN DEFAULT FALSE;
-- Create index for efficient querying of stateful jobs
CREATE INDEX idx_job_queue_queue_name_stateful ON job_queue (queue_name, stateful);
2. Create Stateful Annotation
Create a marker annotation to identify stateful job processors:
/**
* Marker annotation to indicate a job processor is stateful.
* Only one job with the same queue name marked as stateful
* will be allowed to execute at a time.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Stateful {
// No methods needed - this is a marker annotation
}
3. Update JobQueue Interface
Extend the JobQueue interface to support creating stateful jobs:
/**
* Creates a new job in the queue with the stateful flag.
*
* @param queueName The name of the queue
* @param parameters The job parameters
* @param stateful Whether the job is stateful
* @return The ID of the created job
* @throws JobQueueException if there's an error creating the job
*/
String createJob(String queueName, Map<String, Object> parameters, boolean stateful)
throws JobQueueException;
/**
* Default implementation for backward compatibility.
*/
default String createJob(String queueName, Map<String, Object> parameters)
throws JobQueueException {
return createJob(queueName, parameters, false);
}
4. Update PostgresJobQueue Implementation
4.1. Modify createJob method:
@Override
@WrapInTransaction
public String createJob(final String queueName, final Map<String, Object> parameters, final boolean stateful)
throws JobQueueException {
// Existing implementation...
// Update the CREATE_JOB_QUEUE_QUERY SQL to include the stateful parameter
final static String CREATE_JOB_QUEUE_QUERY =
"INSERT INTO job_queue (id, queue_name, state, created_at, stateful) VALUES (?, ?, ?, ?, ?)";
// Add stateful parameter to query execution
new DotConnect().setSQL(CREATE_JOB_QUEUE_QUERY)
.addParam(jobId)
.addParam(queueName)
.addParam(jobState)
.addParam(now)
.addParam(stateful) // Add the stateful flag
.loadResult();
// Rest of implementation...
}
4.2. Update nextJob method:
@Override
@CloseDBIfOpened
public Job nextJob() throws JobQueueDataException, JobLockingException {
try {
// Modified query to respect stateful job constraints
String query = "WITH stateful_running AS (" +
"SELECT queue_name FROM job_queue WHERE stateful = true AND state = ? " +
") " +
"UPDATE job_queue SET state = ? " +
"WHERE id = (" +
"SELECT jq.id FROM job_queue jq " +
"LEFT JOIN stateful_running sr ON jq.queue_name = sr.queue_name " +
"WHERE jq.state = ? " +
"AND (sr.queue_name IS NULL OR jq.stateful = false) " +
"ORDER BY jq.priority DESC, jq.created_at ASC LIMIT 1 " +
"FOR UPDATE SKIP LOCKED) " +
"RETURNING *";
DotConnect dc = new DotConnect();
dc.setSQL(query);
dc.addParam(JobState.RUNNING.name());
dc.addParam(JobState.RUNNING.name());
dc.addParam(JobState.PENDING.name());
List<Map<String, Object>> results = dc.loadObjectResults();
if (!results.isEmpty()) {
String jobId = (String) results.get(0).get("id");
return getJob(jobId);
}
return null;
} catch (DotDataException e) {
Logger.error(this, "Database error while fetching next job", e);
throw new JobQueueDataException("Database error while fetching next job", e);
} catch (Exception e) {
Logger.error(this, "Error while locking next job", e);
throw new JobLockingException("Error while locking next job: " + e.getMessage());
}
}
5. Update JobQueueManagerAPI Implementation
@Override
public String createJob(final String queueName, final Map<String, Object> parameters)
throws JobProcessorNotFoundException, DotDataException {
// Check if processor is stateful
final Class<? extends JobProcessor> clazz = processors.get(queueName);
if (null == clazz) {
throw new JobProcessorNotFoundException(queueName);
}
boolean isStateful = clazz.isAnnotationPresent(Stateful.class);
try {
return jobQueue.createJob(queueName, parameters, isStateful);
} catch (JobQueueException e) {
throw new DotDataException("Error creating job", e);
}
}
Work Items
-
Create upgrade task for adding stateful flag to
job_queuetable -
Implement Stateful marker annotation
-
Update JobQueue interface to support stateful flag
-
Modify PostgresJobQueue implementation:
- Update
createJobmethod to store stateful flag - Enhance
nextJobmethod to enforce stateful constraints
- Update
-
Update
JobQueueManagerAPIimplementation to detect stateful jobs -
Write unit and integration tests for stateful job behavior
Expected Behavior
- Jobs marked with the
@Statefulannotation will be stored withstateful=truein the database - If a stateful job of a specific queue name is running, no other stateful job of the same queue name will be started until the running job completes
- Non-stateful jobs can run concurrently with stateful jobs of the same queue name
- The underlying locking mechanism will still use "FOR UPDATE SKIP LOCKED" to ensure efficient job selection in concurrent environments
Technical Considerations
- The implementation leverages PostgreSQL's transaction capabilities for coordination
- The approach maintains backward compatibility with existing code
- The change is minimally invasive to the current architecture
- Performance impact should be negligible as we're using efficient SQL queries and indexes
Proposed Objective
Core Features
Proposed Priority
Priority 2 - Important
Acceptance Criteria
- Developers can mark job processors as stateful using the
@Statefulannotation - The system prevents concurrent execution of stateful jobs of the same queue name
- Non-stateful jobs are unaffected by this change and maintain current behavior
- All existing unit and integration tests continue to pass
- New tests specifically for stateful job behavior pass
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the JobQueue interface, PostgresJobQueue implementation, and JobQueueManagerAPI, then inspect the job_queue schema and existing queue behavior. Add the Stateful annotation, persistence and selection constraints, and unit and integration coverage so stateful jobs do not run concurrently while non-stateful jobs remain unaffected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, postgresql
- Domain
- backend, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100