JobService.java

1
package edu.ucsb.cs156.jobs.services;
2
3
import edu.ucsb.cs156.jobs.entities.Job;
4
import edu.ucsb.cs156.jobs.entities.JobLog;
5
import edu.ucsb.cs156.jobs.errors.EntityNotFoundException;
6
import edu.ucsb.cs156.jobs.repositories.JobLogRepository;
7
import edu.ucsb.cs156.jobs.repositories.JobsRepository;
8
import java.util.ArrayList;
9
import java.util.Collections;
10
import java.util.List;
11
import java.util.stream.Collectors;
12
import lombok.extern.slf4j.Slf4j;
13
import org.springframework.beans.factory.annotation.Autowired;
14
import org.springframework.context.annotation.Lazy;
15
import org.springframework.scheduling.annotation.Async;
16
import org.springframework.transaction.support.TransactionTemplate;
17
18
/**
19
 * Creates {@link Job} rows and runs {@link JobContextConsumer}s asynchronously on the {@code
20
 * jobsExecutor}, recording status and log output as they run.
21
 */
22
@Slf4j
23
public class JobService {
24
  @Autowired private JobsRepository jobsRepository;
25
26
  @Autowired private JobLogRepository jobLogRepository;
27
28
  @Autowired private JobUserProvider jobUserProvider;
29
30
  @Autowired private JobContextFactory contextFactory;
31
32
  /*
33
   * This is a self-referential bean so that runJobAsync is invoked through the
34
   * Spring proxy; a plain this.runJobAsync(...) call would bypass @Async.
35
   */
36
  @Lazy @Autowired private JobService self;
37
38
  @Autowired private TransactionTemplate transactionTemplate;
39
40
  public Job runAsJob(JobContextConsumer jobFunction) {
41
    Job job =
42
        Job.builder()
43
            .createdById(jobUserProvider.getCurrentUserId())
44
            .createdByEmail(jobUserProvider.getCurrentUserEmail())
45
            .status("queued")
46
            .jobName(jobFunction.getJobName())
47
            .scopeType(jobFunction.getScopeType())
48
            .scopeId(jobFunction.getScopeId())
49
            .build();
50
51
    jobsRepository.save(job);
52
    log.info("Queued job: {}, jobName={}", job.getId(), job.getJobName());
53 1 1. runAsJob : removed call to edu/ucsb/cs156/jobs/services/JobService::runJobAsync → KILLED
    self.runJobAsync(job, jobFunction);
54
55 1 1. runAsJob : replaced return value with null for edu/ucsb/cs156/jobs/services/JobService::runAsJob → KILLED
    return job;
56
  }
57
58
  /**
59
   * Runs a job asynchronously.
60
   *
61
   * <p>This method uses a TransactionTemplate because outside of the Spring context, you cannot
62
   * delete entities that are unmanaged by Hibernate. Using the transactionTemplate lambda keeps the
63
   * database session open and allows Hibernate to maintain its knowledge of the object graph (i.e.
64
   * the entities).
65
   *
66
   * <p>Note that using the transactionTemplate lambda means that if there is an unhandled
67
   * exception, either every database transaction succeeds, or all of them are rolled back.
68
   *
69
   * <p>However, the job entity metadata will still be saved.
70
   *
71
   * @param job metadata entity about the job
72
   * @param jobFunction runnable job function
73
   */
74
  @Async("jobsExecutor")
75
  public void runJobAsync(Job job, JobContextConsumer jobFunction) {
76
    /*
77
     * The job may have waited in the executor queue (it runs one job at a
78
     * time by default); "running" is only truthful once we get here. This
79
     * save is outside the wrapping transaction, so it is visible immediately.
80
     */
81 1 1. runJobAsync : removed call to edu/ucsb/cs156/jobs/entities/Job::setStatus → KILLED
    job.setStatus("running");
82
    jobsRepository.save(job);
83
84
    JobContext context = contextFactory.createContext(job);
85
86
    try {
87 1 1. runJobAsync : removed call to org/springframework/transaction/support/TransactionTemplate::executeWithoutResult → KILLED
      transactionTemplate.executeWithoutResult(
88
          status -> {
89
            try {
90 1 1. lambda$runJobAsync$0 : removed call to edu/ucsb/cs156/jobs/services/JobContextConsumer::accept → KILLED
              jobFunction.accept(context);
91
              /*lambdas cannot throw checked exceptions
92
              have to repackage as a runtime exception
93
              to catch outside transactional boundary*/
94
            } catch (Exception e) {
95
              throw new RuntimeException(e);
96
            }
97
          });
98
    } catch (Exception e) {
99 1 1. runJobAsync : removed call to edu/ucsb/cs156/jobs/entities/Job::setStatus → KILLED
      job.setStatus("error");
100
      jobsRepository.save(job);
101 1 1. runJobAsync : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
      context.log(e.getMessage());
102
      return;
103
    }
104
105 1 1. runJobAsync : removed call to edu/ucsb/cs156/jobs/entities/Job::setStatus → KILLED
    job.setStatus("complete");
106
    jobsRepository.save(job);
107
  }
108
109
  /** The full log for one job, oldest line first. */
110
  public String getJobLogs(Long jobId) {
111 1 1. getJobLogs : negated conditional → KILLED
    if (!jobsRepository.existsById(jobId)) {
112
      throw new EntityNotFoundException(Job.class, jobId);
113
    }
114
    List<JobLog> entries = jobLogRepository.findByJobIdOrderByIdAsc(jobId);
115 1 1. getJobLogs : replaced return value with "" for edu/ucsb/cs156/jobs/services/JobService::getJobLogs → KILLED
    return entries.stream().map(JobLog::getMessage).collect(Collectors.joining("\n"));
116
  }
117
118
  /**
119
   * Everything logged for this job since {@code afterId} (exclusive), oldest first — the
120
   * incremental "tail -f" query: a polling client passes the highest id it has already seen and
121
   * gets back only new lines.
122
   */
123
  public List<JobLog> getJobLogTail(Long jobId, Long afterId) {
124 1 1. getJobLogTail : negated conditional → KILLED
    if (!jobsRepository.existsById(jobId)) {
125
      throw new EntityNotFoundException(Job.class, jobId);
126
    }
127 1 1. getJobLogTail : replaced return value with Collections.emptyList for edu/ucsb/cs156/jobs/services/JobService::getJobLogTail → KILLED
    return jobLogRepository.findByJobIdAndIdGreaterThanOrderByIdAsc(jobId, afterId);
128
  }
129
130
  /**
131
   * The most recent log lines for one job, joined into a single string oldest-first — used to
132
   * populate a preview on list/paginated responses without shipping each job's entire log. Assumes
133
   * the job id is already known-valid (the caller has just fetched a page of {@link Job} rows).
134
   */
135
  public String getJobLogPreview(Long jobId) {
136
    List<JobLog> tail = jobLogRepository.findTop10ByJobIdOrderByIdDesc(jobId);
137
    List<JobLog> chronological = new ArrayList<>(tail);
138 1 1. getJobLogPreview : removed call to java/util/Collections::reverse → KILLED
    Collections.reverse(chronological);
139 1 1. getJobLogPreview : replaced return value with "" for edu/ucsb/cs156/jobs/services/JobService::getJobLogPreview → KILLED
    return chronological.stream().map(JobLog::getMessage).collect(Collectors.joining("\n"));
140
  }
141
}

Mutations

53

1.1
Location : runAsJob
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runAsJob_populates_job_and_dispatches_async()]
removed call to edu/ucsb/cs156/jobs/services/JobService::runJobAsync → KILLED

55

1.1
Location : runAsJob
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runAsJob_leaves_scope_null_for_unscoped_jobs()]
replaced return value with null for edu/ucsb/cs156/jobs/services/JobService::runAsJob → KILLED

81

1.1
Location : runJobAsync
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runJobAsync_success_sets_status_complete_and_commits()]
removed call to edu/ucsb/cs156/jobs/entities/Job::setStatus → KILLED

87

1.1
Location : runJobAsync
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runJobAsync_failure_sets_status_error_logs_and_rolls_back()]
removed call to org/springframework/transaction/support/TransactionTemplate::executeWithoutResult → KILLED

90

1.1
Location : lambda$runJobAsync$0
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runJobAsync_failure_sets_status_error_logs_and_rolls_back()]
removed call to edu/ucsb/cs156/jobs/services/JobContextConsumer::accept → KILLED

99

1.1
Location : runJobAsync
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runJobAsync_failure_sets_status_error_logs_and_rolls_back()]
removed call to edu/ucsb/cs156/jobs/entities/Job::setStatus → KILLED

101

1.1
Location : runJobAsync
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runJobAsync_failure_sets_status_error_logs_and_rolls_back()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

105

1.1
Location : runJobAsync
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:runJobAsync_success_sets_status_complete_and_commits()]
removed call to edu/ucsb/cs156/jobs/entities/Job::setStatus → KILLED

111

1.1
Location : getJobLogs
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:getJobLogs_returns_empty_string_when_no_lines_logged()]
negated conditional → KILLED

115

1.1
Location : getJobLogs
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:getJobLogs_joins_log_lines_in_order()]
replaced return value with "" for edu/ucsb/cs156/jobs/services/JobService::getJobLogs → KILLED

124

1.1
Location : getJobLogTail
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:getJobLogTail_throws_EntityNotFoundException_when_missing()]
negated conditional → KILLED

127

1.1
Location : getJobLogTail
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:getJobLogTail_returns_lines_after_the_given_id()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/jobs/services/JobService::getJobLogTail → KILLED

138

1.1
Location : getJobLogPreview
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:getJobLogPreview_reverses_the_newest_first_query_to_chronological_order()]
removed call to java/util/Collections::reverse → KILLED

139

1.1
Location : getJobLogPreview
Killed by : edu.ucsb.cs156.jobs.services.JobServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.jobs.services.JobServiceTests]/[method:getJobLogPreview_reverses_the_newest_first_query_to_chronological_order()]
replaced return value with "" for edu/ucsb/cs156/jobs/services/JobService::getJobLogPreview → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0