/ / Jak zrobić długotrwałą @ Zaplanowaną metodę sprężyny, hibernacja działa? - wiosna, hibernacja, transakcje, autocommit, wiosenny plan

Jak zrobić długotrwałą metodę @ Zaplanowanej wiosny, hibernacja działa? - wiosna, hibernacja, transakcje, autocommit, wiosenny plan

Próbuję zrobić usługę sieciową Jersey, która pozwalaklientów do tworzenia zadań. Te zadania są przechowywane w bazie danych, przy użyciu Hibernuj jako dostawca trwałości. Zadania będą wykonywane w tle przez usługę Zaplanowane, którą chciałbym zaplanować za pomocą Spring.

Stworzyłem metodę Spring Scheduled, taką jak ta:

@Service
public class MyTimedService
{
@Inject
IJobs allJobs;

private static final Logger LOG = LoggerFactory.getLogger( MyTimedService.class );


@Scheduled(fixedRate=5000)
public void processJobs()
{
for(BaseJob job: allJobs.getQueuedJobs())
{
processJob(job, new JobContext());
}
}


private void processJob( final BaseJob job, JobContext context ) throws JobException
{
job.start();

LOG.info( "Starting: " + job.getName() );
job.execute( context );
LOG.info( "Finished: " + job.getName() );

if ( job.getErrors().size() > 0 )
{
Throwable e = job.getErrors().get( 0 );
throw new JobException( e );
}
job.finished();

}
...
}

Ponieważ zadanie będzie działać przez długi czas, jajakoś zrobić, aby job.start () zgłosił zmianę stanu (z QUEUE na IN_PROGRESS) do bazy danych. Wcześniej używałem implementacji wiersza poleceń i miałem własne zarządzanie transakcjami begin() i commit() tuż koło job.start().

Teraz muszę sprawić, żeby działało, używając Spring ...

Jakieś rady, jak oddzielić obawy i sprawić, by to działało?

Odpowiedzi:

1 dla odpowiedzi № 1

Edytować

Jedną z rzeczy, której naprawdę nie rozumiem, jest to, dlaczego doWork potrzebuje jednej dużej transakcji.

To nie musi być. Istnieją zastrzeżenia w obu kierunkach. Zauważyłem niektóre z nich w poprawionym zdarzeniu klasy (JobRunnerService) powyżej metody doWork (...). Te notatki są warte ... zauważenia.

To, co chciałbym osiągnąć, to to, że doWork regularnie może ustawić postęp pracy

To może ale nie musi być trudne do osiągnięciaw zależności od tego, czy chcesz, aby robota (...) była powiązana z transakcją i czy każda praca może zostać rozbita w ten sam sposób (tj. aktualizacje zawsze pojawiałyby się w statycznej lokalizacji w kodzie). Nie znam wszystkich twoich wymagań, więc nie mogę odpowiedzieć na to pytanie. Chciałbym jednak powtórzyć moją radę na temat wglądu w Spring Batch.

JobRunnerService

import me.mike.jobs.model.Job;
import me.mike.jobs.model.JobState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

/**
* !!This bean is STATEFUL!!
*/
@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class JobRunnerService {
@Autowired
private JobService js;

public void processJob(Job job) {
job.setState(JobState.WORKING_0);
js.update(job);
try {
doWork(job);
job.setState(JobState.COMPLETE);
} catch (Exception e) {
job.setState(JobState.FAILED);
}
System.out.println("I"m done working.");
js.update(job);
}

/**
* Be sure that any unchecked exception you throw gets added into the "rollbackFor" since it won"t trigger
* a rollback if you don"t...
*
* The @Transactional is optional - I assumed you would want the work performed in the job to be transactional.
*
* Note: Remember, when doing the work represented by these jobs, that your EntityManager (or SessionFactory) is
* configured with a TransactionManager and, as such, will throw exceptions when you attempt to do work within them
* without a Transaction.  You will either need a separate EntityManager (SessionFactory) or something like a
* JdbcTemplate.
*
* Note: If the Job"s work DOES need to be Transactional, this will probably not work.  A very simple solution
* would to be to split up the work within the job into "steps" or "stages."  The processJob(...) method above
* could then call each stage and, at the conclusion, update the Job"s state appropriately.  This, of course,
* would not work if each Job had N number of stages where N could vary an indeterminate amount.
*/
//@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = { IllegalArgumentException.class })
public void doWork(Job job) throws IllegalArgumentException {
// This method begins its own transaction, every single time its called.  Period.
// Do some work...
job.setState(JobState.WORKING_10);
js.update(job);
// Do more work...
job.setState(JobState.WORKING_90);
js.update(job);
// At the conclusion, the transaction bound to this method is committed, unless a rollback was initiated.
}
}

Przedmowa: Sądzę, że mądrze by było, gdybyś spojrzał na coś takiego jak SpringBatch. Może wymagać więcej konfiguracji, ale zapewnia też o wiele więcej wsparcia.

Jeśli dobrze cię rozumiem, chcesz zachować"Zadania" w tabeli (tworzenie REST-owe). Chcesz @ zaplanowane zadanie, które może okresowo działać w tle, aby wykonać pracę, która reprezentuje każde z tych zadań. Następnie chcesz zmienić stan (heh) na każdej z tych jednostek przed i po pracy nad nimi. Ograniczeniem jest to, że początkowa zmiana stanu musi nastąpić w obrębie własnych granic transakcyjnych, podobnie jak nieunikniona zmiana stanu końcowego.

Uruchomiłem ten kod przeciwko MySQL 5.x DB przy użyciu Spring, JPA i Hibernate .Jeśli potrzebujesz, mogę podać dla ciebie mój plik applicationContext i moje pliki xml-servlet.

Spowoduje to wykonanie tego, co rozumiem przez Twoje cele:

Model:

import org.hibernate.validator.constraints.Length;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.UUID;

@Entity
public class Job {
@Id
private String id;

@Column
@NotNull
@Length(min = 3, max = 50)
private String name;

@Enumerated(EnumType.STRING)
@Column(length = 50, nullable = false)
private JobState state;

public UUID getId() {
return UUID.fromString(id);
}

public void setId(UUID id) {
this.id = id.toString();
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public JobState getState() {
return state;
}

public void setState(JobState state) {
this.state = state;
}
}

Repozytorium:

import me.mike.jobs.model.Job;
import me.mike.jobs.model.JobState;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Repository
public class JobDao {
@PersistenceContext
private EntityManager em;


@Transactional(propagation = Propagation.REQUIRED)
public void create(Job job) {
// ...
}

@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
public Set<Job> readAll() {
// ...
}

@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
public Job readById(UUID id) {
// ...
}

@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
public Set<Job> readByState(JobState state) {
// ...
}

@Transactional(propagation = Propagation.REQUIRED)
public void update(Job job) {
// ...
}

@Transactional(propagation = Propagation.REQUIRED)
public void delete(Job job) {
// ...
}
}

Usługa JobService (To obsługuje RESTful działania w twojej jednostce Job)

import me.mike.jobs.dao.JobDao;
import me.mike.jobs.model.Job;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;

@Service
public class JobService {
@Autowired
private JobDao jd;

@Transactional(propagation = Propagation.REQUIRED)
public void create(Job job) {
// Business logic...
jd.create(job);
// More business logic...
}

@Transactional(propagation = Propagation.REQUIRED, readOnly = true)
public Set<Job> read() {
// Business logic...
Set<Job> jobs = jd.readAll();
// More business logic...
return jobs;
}

@Transactional(propagation = Propagation.REQUIRED)
public void update(Job job) {
// Business logic...
jd.update(job);
// More business logic...
}

@Transactional(propagation = Propagation.REQUIRED)
public void delete(Job job) {
// Business logic...
jd.delete(job);
// More business logic...
}
}

Usługa MaintenanceService (Ten facet trzymałby wszystkie twoje metody @ScheduledTask)

import me.mike.jobs.dao.JobDao;
import me.mike.jobs.model.Job;
import me.mike.jobs.model.JobState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class MaintenanceService {
@Autowired
private JobRunnerService jrs;

@Autowired
private JobDao jd;

@Scheduled(fixedDelay = 5000, initialDelay = 5000)
public void processQueuedJobs() {
// This may be somewhat dangerous depending on how many jobs could potentially be racked up during the "downtime"
for (Job curJob : jd.readByState(JobState.QUEUED))
jrs.processJob(curJob);
}

// Any other timed service methods...
}

JobRunnerService To jest Usługa, która faktycznie wykonuje zadania

import me.mike.jobs.model.Job;
import me.mike.jobs.model.JobState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
* !!This bean is STATEFUL!!
*/
@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class JobRunnerService {
@Autowired
private JobService js;

public void processJob(Job job) {
job.setState(JobState.WORKING);
js.update(job);
try {
doWork(job);
job.setState(JobState.COMPLETE);
} catch (Exception e) {
job.setState(JobState.FAILED);
}
System.out.println("I"m done working.");
js.update(job);
}

/**
* Be sure that any unchecked exception you throw gets added into the "rollbackFor" since it won"t trigger
* a rollback if you don"t...
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = { IllegalArgumentException.class })
public void doWork(Job job) throws IllegalArgumentException {
// This method begins its own transaction, every single time its called.  Period.
// Do your work here...
// At the conclusion, the transaction bound to this method is committed, unless a rollback was initiated.
}
}

1 dla odpowiedzi nr 2

Zakładam, że masz włączone zarządzanie transakcjami z adnotacjami w konfiguracji wiosennej

@Service
public class MyTimedService {

@Inject
IJobs allJobs;

@Inject
JobService jobService;

private static final Logger LOG = LoggerFactory.getLogger( MyTimedService.class );

@Scheduled(fixedRate=5000)
public void processJobs() {
for(BaseJob job: allJobs.getQueuedJobs()) {
processJob(job, new JobContext());
}
}

private void processJob( final BaseJob job, JobContext context ) throws JobException {
jobService.start(job);

LOG.info( "Starting: " + job.getName() );
job.execute( context );
LOG.info( "Finished: " + job.getName() );

if ( job.getErrors().size() > 0 ) {
Throwable e = job.getErrors().get( 0 );
throw new JobException( e );
}

jobService.complete(job);

}

}

@Service
public class JobService {

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void start(BaseJob job){
job.start();
}

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void complete(BaseJob job){
job.finished();
}

}

Kolejny punkt, o którym należy pamiętać

Jeśli istnieje wyjątek podczas przetwarzania zadania, jego status pozostanie IN_PROGRESS zamiast czegoś podobnego COMPLETED_WITH_EXCEPTION.


0 dla odpowiedzi № 3

Zanim zasugeruję mój pomysł, powinienem powiedzieć, że to, co opisujesz jako problem, jest dość ogólne i można sobie z nim poradzić z różnymi perspektywami. Staram się wykorzystywać Twój kod tak często, jak to możliwe.

  1. Skonfiguruj Wiosenne transakcje (wiosna-tx) moduł dla twojego projektu. Pozwala to na użycie @Transactional w sprawie metod trwałych transakcji.
  2. Zakładam, że to, co oznaczacie przez IJobs to repozytorium zadań, które następuje po standardowej, wiosennej implementacji, takiej jak Wiosenny WZP lub Wiosenne repozytoria
  3. Ponownie wykorzystam Twój kod w następujący sposób
    • Spróbuj oddzielić model, który reprezentuje pracę i jest utrzymywany (JobModel) z obiektu reprezentującego wykonywalne zadanie (ExecutableJob). Możesz mieć prostą metodę zmapowania tych dwóch elementów razem.
    • Ustaw "najmniejszy" możliwy blok kodu, aby był "transakcyjny". metoda updateJobStatus ma jedną odpowiedzialność, która aktualizuje status pracy.
    • Użyj metody, która aktualizuje status zadania w miejscujest konieczne. Obejmuje to rozpoczęcie pracy, ukończenie zadania z powodzeniem i sytuację, w której albo zadanie zakończyło się niepowodzeniem, albo może wystąpić wyjątek środowiska wykonawczego, a użytkownik chce ponownie zgłosić status.

Ponownie użyty schemat kodu:

@Service
public class LongRunningJobService {

@Inject
JobRepository jobs; // IJobs

@Scheduled(fixedDelay = 60000)
public void processJobs() {
for (JobModel j : jobs.getQueuedJobs()) {
JobContext context = null;
processJob(j, context);
}
}

protected void processJob(JobModel jobModel, JobContext context) {
// update the status of the job
updateJobStatus(jobModel, JobStatus.RUNNING);

ExecutableJob job = null; // createJob(jobModel);
job.execute(context);

// process job results
// if necessary, catch exceptions and again update job status

// success
updateJobStatus(jobModel, JobStatus.FINISHED);

}

@Transactional
protected void updateJobStatus(JobModel jobModel, JobStatus status) {
jobs.updateJobStatus(jobModel, status);
}

static enum JobStatus {
QUEUED, RUNNING, FINISHED;
}

}