I have multiple quartz workers
Each worker picks a db record (printer) and then doing some work on it (reading info from the printer over the network).
It can take up to 30 sec to 1 min to complete each job.
Back in JDBC days I would run (pseudo code)
printer = "select from printer where status=? for update"
do the work, (takes 1 min)
update the printer record.
My question is this approach with PESSIMISTIC_WRITE is ok:
public interface PrinterRepo extends CrudRepository<Printer,String>
{
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT r FROM Printers r where r.status = :status")
Printer findOneAndLock(@Param("status")String status);
}
Then the worker:
@Transactional
public void execute(JobExecutionContext jobExecutionContext) {
Printer p = printerRepo.findOneAndLock("status");
//do the work here (30 sec to 1 min)
printerRepo.save(p);
}
For my understanding lock will be release at the end of the function annotated with @Transactional correct?
My question is what will happen to other workers?
Will they starve while waiting for findOneAndLock?
Thank you
At work we use Hibernate JDBCTemplate because it has more flexibility. It also has better performance than JPA because you are not "loading" a lot of unnecessary data into your app. In the JDBCTemplate case, your SQL skills go a long way in giving you exactly what you need at the right speed.
PessimisticLockException indicates that obtaining a lock or converting a shared to exclusive lock fails and results in a transaction-level rollback. LockTimeoutException indicates that obtaining a lock or converting a shared lock to exclusive times out and results in a statement-level rollback.
JPA uses EntityManager interface to create/read/delete operation and maintains the persistence context. Hibernate uses Session interface to create/read/delete operation and maintains the persistence context. JPA uses JPQL (Java Persistence Query Language) as Object Oriented Query language for database operations.
Regardless of which type and level of locks you are going to use, and what will happen to other workers, the long-term lock, as well as long-term transaction, is not a good solution. IMHO in your case is better to use different approach without any locks, for example, an additional table to record the printer 'locks':
create table printer_locks (
printer_id bigint not null constraint printer_locks_pkey primary key,
created_at timestamp not null,
worker_id bigint not null constraint fk_printer_locks_worker_id references workers,
constraint fk_printer_locks_printer_id foreign key (printer_id) references printers(id)
);
When a worker wants to start the job with some printer, first it tries to insert the record to this table. Then, in case of success, it starts the job. When the job is completed then the worker deletes this record.
Because the printer_id
column is unique - other workers will not be able to start working with the same printer at the same time.
Implementation:
@Entity
@Table(name = "workers")
public class Worker {
@Id
@GeneratedValue(...)
private Long id;
// other stuff...
}
@Entity
@Table(name = "printers")
public class Printer {
@Id
@GeneratedValue(...)
private Long id;
// other stuff...
}
@Entity
@Table(name = "printer_locks")
public class PrinterLock {
@Id
private Long id;
private Instant createdAt = Instant.now();
@OneToOne(fetch = FetchType.LAZY)
@MapsId
private Printer printer;
@ManyToOne(fetch = FetchType.LAZY)
private Worker worker;
public PrinterLock(Printer printer, Worker worker) {
this.printer = printer;
this.worker = worker;
}
// other stuff...
}
public void execute(...) {
Worker worker = ...;
Long printerId = ...;
printerRepo.findById(printerId)
.map(printer -> {
try {
printerLockRepo.save(new PrinterLock(printer, worker));
try {
// do the work here (30 sec to 1 min)
} finally {
printerLockRepo.deleteById(printerId);
}
} catch(Exception e) {
log.warn("Printer {} is busy", printerId);
}
})
.orElseThrow(() -> new PrinterNotFoundException(printerId));
}
Note that the execute
method even doesn't have @Transactional
annotation.
An additional advantage of this approach is the column createdAt
which allow you to control hanging jobs.
Further reading:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With