Skip to content

Commit

Permalink
(feat) enable Spring Batch support, add a batch to backup users and m…
Browse files Browse the repository at this point in the history
…ake some stats
  • Loading branch information
jonathanlermitage committed Oct 25, 2018
1 parent 4fa667d commit c0e3422
Show file tree
Hide file tree
Showing 38 changed files with 970 additions and 11 deletions.
7 changes: 5 additions & 2 deletions DEPLOY.md
Expand Up @@ -5,7 +5,9 @@ First, go to project's root and make the `do.sh` utility script executable: `chm

### Manually

* Install **JDK8** and **MongoDB 4.0.x** (it should work with any version from 3.4.x to 4.1.x). MongoDB should listen on port 27017, with no authentication. See `src/main/resources/application-dev.yml` for details.
* Install latest **JDK8**.
* Install **MongoDB** (any version from 3.4.x to 4.1.x should work). Make it listen on port 27017, with no authentication. See `src/main/resources/application-dev.yml` for details.
* Install **MariaDB** or MySQL (any reasonably recent version should work).
* Package and run application via `do rd`. Application will start on port 8080 with `dev` Spring profile.
* To run with another Spring profile (e.g. `prod`), package application via `do p`, go to `target/` directory and run `java -jar -Xms128m -Xmx512m -Dspring.profiles.active=prod,metrics manon.jar`.

Expand Down Expand Up @@ -48,4 +50,5 @@ Application dockerized with [Jib](https://github.com/GoogleContainerTools/jib) a
```
* Check application connectivity by visiting `http://localhost:8080/actuator/health` (default login/password is `ROOT/woot`).
* Replace `8080` by `8000` to access application via Nginx proxy.
* You can check Nginx error and access logs in `~/manon-nginx-logs`.
* Check Nginx error and access logs in `~/manon-nginx-logs`.
* Launch a batch (e.g. `userSnapshotJob`) `curl -X POST http://localhost:8000/api/v1/sys/batch/start/userSnapshotJob --user ROOT:woot` then check the `UsersStats` and `UserSnapshot` MongoDB collections.
19 changes: 16 additions & 3 deletions docker-compose.yml
Expand Up @@ -2,7 +2,7 @@ version: '3'

services:

# main application
# Main application
manon:
container_name: manon
image: lermitage-manon:1.0.0-SNAPSHOT
Expand All @@ -12,8 +12,9 @@ services:
- ~/manon-app-logs:/logs
depends_on:
- mongo
- maria

# database
# MongoDB database
mongo:
container_name: mongo
image: mongo:4.0.3-xenial
Expand All @@ -27,7 +28,19 @@ services:
MONGO_INITDB_ROOT_PASSWORD: woot
MONGO_INITDB_DATABASE: admin

# http proxy
# MariaDB database
maria:
container_name: maria
image: mariadb:10.3.10-bionic
ports:
- "3306:3306"
volumes:
- ~/manon-maria-db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: woot
MYSQL_DATABASE: manon

# HTTP proxy
nginx:
container_name: nginx
image: nginx:1.15.5
Expand Down
22 changes: 22 additions & 0 deletions pom.xml
Expand Up @@ -20,9 +20,11 @@
<profileArgLine>-Dspring.profiles.active=test,metrics</profileArgLine>

<embed.mongo.version>2.1.1</embed.mongo.version>
<hsqldb.version>2.4.1</hsqldb.version>
<janino.version>3.0.10</janino.version>
<logback-classic.version>1.2.3</logback-classic.version>
<lombok.version>1.18.2</lombok.version>
<mariadb.version>2.3.0</mariadb.version>
<mockito-core.version>2.23.0</mockito-core.version> <!-- TODO forced to 2.21+ to support jdk11. Remove it once spring-boot 2.1 is released. -->
<rest-assured.version>3.2.0</rest-assured.version>
<slf4j-api.version>1.7.25</slf4j-api.version>
Expand Down Expand Up @@ -84,7 +86,27 @@
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
<version>${spring-boot.version}</version>
</dependency>

<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!-- SQL. -->
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>${mariadb.version}</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
<scope>test</scope>
</dependency>

<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!-- Logging. -->
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/manon/Application.java
Expand Up @@ -4,6 +4,7 @@
import manon.app.trace.service.AppTraceService;
import manon.user.err.UserExistsException;
import manon.user.service.RegistrationService;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
Expand All @@ -20,6 +21,7 @@

@SpringBootApplication
@EnableMongoAuditing
@EnableBatchProcessing
@EnableScheduling
@RequiredArgsConstructor
public class Application extends SpringBootServletInitializer {
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/manon/app/batch/api/TaskRunnerWS.java
@@ -0,0 +1,43 @@
package manon.app.batch.api;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import manon.app.batch.err.TaskNotFoundException;
import manon.app.batch.model.TaskStatus;
import manon.app.batch.service.TaskRunnerService;
import manon.app.security.UserSimpleDetails;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static manon.app.config.API.API_SYS;

/** Task API. */
@RestController
@RequestMapping(value = API_SYS)
@RequiredArgsConstructor
@Slf4j
public class TaskRunnerWS {

private final TaskRunnerService taskRunnerService;

@PostMapping(value = "/batch/start/{task}")
public TaskStatus startTask(@AuthenticationPrincipal UserSimpleDetails sys, @PathVariable("task") String task)
throws TaskNotFoundException, JobParametersInvalidException, JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException {
log.warn("admin {} starts task {}", sys.getUsername(), task);
ExitStatus exitStatus = taskRunnerService.run(task);
return TaskStatus.builder()
.running(exitStatus.isRunning())
.exitCode(exitStatus.getExitCode())
.exitDescription(exitStatus.getExitDescription())
.build();
}
}
25 changes: 25 additions & 0 deletions src/main/java/manon/app/batch/err/BatchControllerAdvice.java
@@ -0,0 +1,25 @@
package manon.app.batch.err;

import manon.app.config.ControllerAdviceBase;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.Map;

import static org.springframework.http.HttpStatus.NOT_FOUND;

@ControllerAdvice
public class BatchControllerAdvice extends ControllerAdviceBase {

@ExceptionHandler(TaskNotFoundException.class)
@ResponseStatus(NOT_FOUND)
@ResponseBody
public Map<String, Object> handle(TaskNotFoundException error) {
Map<String, Object> map = error();
map.put(FIELD_ERRORS, error.getClass().getSimpleName());
map.put(FIELD_MESSAGE, error.getName());
return map;
}
}
12 changes: 12 additions & 0 deletions src/main/java/manon/app/batch/err/TaskNotFoundException.java
@@ -0,0 +1,12 @@
package manon.app.batch.err;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("serial")
@AllArgsConstructor
@Getter
public class TaskNotFoundException extends Exception {

private final String name;
}
19 changes: 19 additions & 0 deletions src/main/java/manon/app/batch/listener/TaskListener.java
@@ -0,0 +1,19 @@
package manon.app.batch.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class TaskListener extends JobExecutionListenerSupport {

@Override
public void afterJob(JobExecution jobExecution) {
super.afterJob(jobExecution);
log.info("job {} ended in state {}",
jobExecution.getJobInstance().getJobName(),
jobExecution.getStatus());
}
}
21 changes: 21 additions & 0 deletions src/main/java/manon/app/batch/model/TaskStatus.java
@@ -0,0 +1,21 @@
package manon.app.batch.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Getter
@ToString
@Builder(toBuilder = true)
public class TaskStatus {

private boolean running;
private String exitCode;
private String exitDescription;
}
15 changes: 15 additions & 0 deletions src/main/java/manon/app/batch/service/TaskRunnerService.java
@@ -0,0 +1,15 @@
package manon.app.batch.service;

import manon.app.batch.err.TaskNotFoundException;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;

public interface TaskRunnerService {

ExitStatus run(String task) throws TaskNotFoundException,
JobParametersInvalidException, JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException;
}
52 changes: 52 additions & 0 deletions src/main/java/manon/app/batch/service/TaskRunnerServiceImpl.java
@@ -0,0 +1,52 @@
package manon.app.batch.service;

import lombok.RequiredArgsConstructor;
import manon.app.batch.err.TaskNotFoundException;
import manon.user.batch.UserSnapshotTask;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Date;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class TaskRunnerServiceImpl implements InitializingBean, TaskRunnerService {

private final JobLauncher launcher;

@Qualifier(UserSnapshotTask.JOB_NAME)
private final Job userSnapshotJob;

private Map<String, Job> jobs;

@Override
public void afterPropertiesSet() {
this.jobs = Collections.singletonMap(UserSnapshotTask.JOB_NAME, userSnapshotJob);
}

@Override
public ExitStatus run(String task) throws TaskNotFoundException,
JobParametersInvalidException, JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException {
if (jobs.containsKey(task)) {
return launcher.run(jobs.get(task), todayDateJobParameters()).getExitStatus();
}
throw new TaskNotFoundException(task);
}

private JobParameters todayDateJobParameters() {
return new JobParametersBuilder().addDate("START_DATE", new Date()).toJobParameters();
}
}
9 changes: 5 additions & 4 deletions src/main/java/manon/app/config/API.java
@@ -1,5 +1,10 @@
package manon.app.config;

import lombok.NoArgsConstructor;

import static lombok.AccessLevel.PRIVATE;

@NoArgsConstructor(access = PRIVATE)
public final class API {

public static final String API_V1 = "/api/v1";
Expand All @@ -21,8 +26,4 @@ public final class API {

/** System trace. */
public static final String API_SYS = API_V1 + "/sys";

private API() {
// utility class
}
}

0 comments on commit c0e3422

Please sign in to comment.