JobContext.java
package edu.ucsb.cs156.jobs.services;
import edu.ucsb.cs156.jobs.entities.Job;
import edu.ucsb.cs156.jobs.entities.JobLog;
import edu.ucsb.cs156.jobs.repositories.JobLogRepository;
import edu.ucsb.cs156.jobs.repositories.JobsRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Handed to a running {@link JobContextConsumer}; each {@link #log} call appends one line to the
* job's log, so admins can watch progress live.
*
* <p>Since v0.2.0, each line is its own row in {@code job_logs} (see {@link JobLog}) rather than a
* read-modify-write of one growing TEXT column on {@code jobs} — an O(1) insert instead of an O(N)
* rewrite of everything logged so far. The job body runs inside one all-or-nothing transaction (see
* {@link JobService#runJobAsync}), so log writes must NOT join it: they would be invisible to the
* admin UI until the whole job commits. When a {@code logTransactionTemplate} is provided
* (configured REQUIRES_NEW by {@link JobContextFactory}), each line commits in its own transaction
* immediately.
*
* <p>A null {@code jobLogRepository} is tolerated as a test seam: the log accumulates on {@code
* job}'s (Java-only, {@code @Transient}) {@code log} field instead of being persisted anywhere. The
* apps' job tests conventionally run jobs against {@code new JobContext(null, job)} and assert on
* {@code job.getLog()}; that legacy two-arg constructor is preserved unchanged for exactly this
* reason.
*/
@Slf4j
public class JobContext {
private final JobLogRepository jobLogRepository;
private final Job job;
private final TransactionTemplate logTransactionTemplate;
/**
* @deprecated kept only so existing test code written against v0.1.x ({@code new JobContext(null,
* job)}) keeps compiling; the {@code jobsRepository} parameter is ignored. Use {@link
* #JobContext(JobLogRepository, Job, TransactionTemplate)}.
*/
@Deprecated
public JobContext(JobsRepository jobsRepository, Job job) {
this((JobLogRepository) null, job, null);
}
public JobContext(
JobLogRepository jobLogRepository, Job job, TransactionTemplate logTransactionTemplate) {
this.jobLogRepository = jobLogRepository;
this.job = job;
this.logTransactionTemplate = logTransactionTemplate;
}
public void log(String message) {
log.info("Job {}: {}", job.getId(), message);
if (jobLogRepository == null) {
// Test seam: no persistence available, so accumulate in-memory on the
// (Java-only) Job.log field, exactly as v0.1.x did for every caller.
String previousLog = job.getLog() == null ? "" : (job.getLog() + "\n");
job.setLog(previousLog + message);
return;
}
JobLog entry = JobLog.builder().jobId(job.getId()).message(message).build();
if (logTransactionTemplate != null) {
logTransactionTemplate.executeWithoutResult(status -> jobLogRepository.save(entry));
} else {
jobLogRepository.save(entry);
}
}
/**
* Exposes the underlying job, mainly so job bodies and their tests can inspect state (e.g. {@code
* ctx.getJob().getLog()}) without threading a separate reference through. Note: for a real
* (non-test-seam) run, {@code job.getLog()} is not kept up to date by {@link #log} any more (see
* class javadoc) — it reflects whatever the caller last set it to, typically nothing.
*/
public Job getJob() {
return job;
}
}