home / 2018.03.26 19:00 /java /cloud /microservices /spring boot
Our objectives for this second part of the workshop will be:
Get a new Spring Boot app started with Spring Initializr, with Web
and MongoDB
dependencies. I called mine com.msdm.files
.
Add it to your project, and let’s start by configuring some defaults,
like database name and the port we start this service on. We’ll want a
different port that our users service for now so we can run both
services on the same machine. Add the following in the application.properties
file:
spring.data.mongodb.database=filesdb
server.port=8090
Next, we’ll create our entity:
package com.msdm.files.entities;
import org.springframework.data.annotation.Id;
import java.util.List;
public class Datafile {
@Id
private String id;
private String name;
private String owner;
private String note;
private List<String> tags;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public List<String> getTags() {
return tags;
}
public void setTags(List<String> tags) {
this.tags = tags;
}
}
And the associated repository:
package com.msdm.files.repositories;
import com.msdm.files.entities.Datafile;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface DatafileRepository extends MongoRepository<Datafile, String>{
}
Now, we need a service that can save a multipart file at a location on disk:
package com.msdm.files.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
@Component
public class FileStorageService {
private static Logger logger = LoggerFactory.getLogger(FileStorageService.class);
@Value("${storage.path}")
private String storagePath;
private File getNewFile(String id) {
Path path = Paths.get(storagePath, id);
return path.toFile();
}
public Optional<File> save(MultipartFile receivedFile, String id) {
try {
File newFile = getNewFile(id);
receivedFile.transferTo(newFile);
return Optional.of(newFile);
} catch (IOException e) {
logger.error(e.getMessage(), e);
return Optional.empty();
}
}
}
First here we notice we have a @Value
annotation. That is the way we insert properties from the application.properties
file in our Java code. This means we will need to add a new line in the application.properties
with the path to the location where we store files: storage.path=C:\\datadir
. Make sure that folder exists on you machine. Next, we transfer the multipart data to a new file on disk. The MultipartFile
datatype gives us a handy method to do this, but we must have a way to
signal the calling code if the operation failed. We use an empty Optional
to signal that file saving has failed.
We can now put this all together ny adding a controller, the basic API to test all this out:
package com.msdm.files.controllers;
import com.msdm.files.entities.Datafile;
import com.msdm.files.repositories.DatafileRepository;
import com.msdm.files.services.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.Optional;
@RestController
@RequestMapping(value = "datafile")
public class DatafileController {
@Autowired
private DatafileRepository datafileRepository;
@Autowired
private FileStorageService fileStorageService;
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> upload(
@RequestParam("owner") String owner,
@RequestParam("file") MultipartFile multipartFile
) {
Datafile datafile = datafileRepository.save(getDatafile(multipartFile, owner));
Optional<File> savedFile = fileStorageService.save(multipartFile, datafile.getId());
if (savedFile.isPresent()) {
return new ResponseEntity(HttpStatus.OK);
} else {
datafileRepository.delete(datafile);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private Datafile getDatafile(MultipartFile multipartFile, String owner) {
Datafile datafile = new Datafile();
datafile.setOwner(owner);
datafile.setName(multipartFile.getOriginalFilename());
return datafile;
}
}
We’ll keep the call basic for now. The controller accepts a POST call
with an owner name/id and a multipart file. First, we save the file
details to MongoDB. This operation will assign a unique ID to our Datafile
entity. We then use that ID to call the FileStorageService
and save the file to disk, if possible. If this operation was successful, we just return an empty 200
response. If saving the file to disk failed, we first make sure to delete the file from MongoDB and then we return an empty 500
response. It’s a stripped down version of the final thing, but it will
let us quickly test if the service works as expected. You can start the
service and test it out, Postman will provide all the required
functionality to make a call to your new service. Upload a simple text
file and verify MongoDB and the local data folder to make sure it all
works.
We could use either one, and since we program the backend of our API, we could make a DELETE method create a new file, if we really wanted to mess with the heads of the people using our API. But if we want to implement a REST API that respects the definition of the REST methods, our options for uploading a file are either POST or PUT, and which is the right one to use depends on the way we implement its operation. The difference between POST and PUT is based on the concept of idempotence. An idempotent operation will bring the system to a clearly defined state based on the parameters of that operation. The state of the system will be the same even if we repeat the call, with the same parameters, multiple times. Let’s say our operation is create an entity with ID and DATA, where ID and DATA are the parameters of the operation. When we make this call, if an entity with ID does not exist, it will be created with the DATA we provided, and if an entity with ID does exist, its data will be replaced with the DATA we provided. So wether we make this call one time, or one hundred times, the system will be in the exact same state at the end: we will have one entity with ID and DATA in our system. According to the definition of REST methods, PUT should be an idempotent operation (as well as GET, DELETE, HEAD, TRACE, OPTIONS). POST, on the other hand, is not required to be an idempotent operation. With POST, we can just provide the DATA and let the system choose an ID. This also means that id we make a POST call with the same DATA multiple consecutive times, we will end up with multiple entities having the same DATA but different IDs.
Now that we clarified this subtle difference we can see that the POST method is the correct one based on the current implementation of the Java function handling the call. If we look back at our user service, we can see that the PUT method used there is not correctly implemented. If the data given to the PUT method does not contain a user ID, a new user will be created. If we make the same call multiple times without an ID, we will end up with multiple users, so the system state changes with each subsequent call. So our PUT implementation is not idempotent. We would have to refuse to service calls to the PUT endpoint when no ID is provided (for example return a BAD_REQUEST). We should also add a POST endpoint for creating new users. We can quickly fix this in our user service:
@RequestMapping(method = RequestMethod.POST, consumes = "application/json")
public ResponseEntity<User> newUser(@RequestParam(value = "email") String email) {
User user = new User(email);
User savedUser = userRepository.save(user);
return new ResponseEntity<User>(savedUser, HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.PUT, consumes = "application/json")
public ResponseEntity<User> saveOrUpdateUser(@RequestBody User user) {
if (user.getId() != null) {
User savedUser = userRepository.save(user);
return new ResponseEntity<User>(savedUser, HttpStatus.OK);
} else {
return new ResponseEntity<User>(HttpStatus.BAD_REQUEST);
}
}
We next implement the file download controller:
package com.msdm.files.controllers;
import com.msdm.files.entities.Datafile;
import com.msdm.files.repositories.DatafileRepository;
import com.msdm.files.services.FileStorageService;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
@RestController
@RequestMapping(value = "download")
public class DownloadController {
private static Logger logger = LoggerFactory.getLogger(DownloadController.class);
@Autowired
private DatafileRepository datafileRepository;
@Autowired
private FileStorageService fileStorageService;
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public void getFile(
@PathVariable("id") String id,
HttpServletResponse response
) {
Datafile datafile = datafileRepository.findOne(id);
if (datafile != null) {
response.addHeader("Content-Type", "application/octet-stream");
response.addHeader("Content-Disposition", getFilenameHeader(datafile));
FileInputStream inputStream = fileStorageService.getInputStream(id);
if (inputStream != null) {
try {
IOUtils.copy(inputStream, response.getOutputStream());
response.flushBuffer();
} catch (IOException e) {
logger.error(e.getMessage(), e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
}
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
}
}
private String getFilenameHeader(Datafile datafile) {
return "attachment; filename=\"" + datafile.getName() + "\"";
}
}
We have a few new elements in the implementation of this controller.
First, the id of the file we are downloading should be provide din the
URL, as part of the path. This is more in line with a REST approach
where entities in your system are viewed as resources, with their unique
URLs. As a future refactoring of the services, it would make sense to
move entity ids in URLs, so our system has a consistent way of
addressing data. We access the id in the path using the @PathVariable
annotation. We also inject the response in the Java method parameters
(Spring will automatically refer the right response object for us). We
first load our file metadata, making sure that the file exists, at leat
in the database, and obtaining the original file name in the process.
Next, we add some headers to the response. The Content-Disposition
header lets us instruct the browser or tool or service downloading the
file what the downloaded file name should be. Finally, we obtain an
input stream from the FileStorageService
and write the contents of that input stream to the response output stream. The new method in FileStorageService
is:
public FileInputStream getInputStream(String id) {
File file = getFile(id);
if (file.exists()) {
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
logger.error(e.getMessage(), e);
}
}
return null;
}
I am just adding the code for the controller that gives us information about all files in our database, just so the code is complete up to this point. This controller is almost identical to the collections controller in the users microservice:
package com.msdm.files.controllers;
import com.msdm.files.entities.Datafile;
import com.msdm.files.repositories.DatafileRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping(value = "datafiles")
public class DatafileCollectionController {
private static int PAGE = 0;
private static int SIZE = 10;
@Autowired
private DatafileRepository datafileRepository;
@RequestMapping(
method = RequestMethod.GET,
produces = "application/json"
)
public List<Datafile> getAllDatafiles(
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size
) {
Pageable pageRequest = getPageRequest(page, size);
if (pageRequest != null) {
Page<Datafile> resultPage = datafileRepository.findAll(pageRequest);
return resultPage.getContent();
} else {
return datafileRepository.findAll();
}
}
private Pageable getPageRequest(Integer page, Integer size) {
if (page != null) {
if (size == null) {
size = SIZE;
}
return new PageRequest(page, size);
} else if (size != null) {
return new PageRequest(PAGE, size);
} else {
return null;
}
}
}
What we need now is to add ability to analyze the datasets we have in the system. This functionality will be split into two services, one to submit analysis requests to a queue and the second service to run the analysis. Since running an analysis is a resource consuming process, we expect that down the line we will need to deploy several analysis running services to ease the load on our system.
The service itself will be very simple; named com.msdm.analysis
, with Web
and MongoDB
dependencies and running on a different port and databse:
spring.data.mongodb.database=analysisdb
server.port=8091
Our entity is the Analysis
, and for now it only needs to store very little information:
package com.msdm.analysis.entities;
import org.springframework.data.annotation.Id;
import java.util.Date;
public class Analysis {
@Id
private String id;
private String owner;
private String fileId;
private Date created;
private Date started;
private Date completed;
private String result;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getFileId() {
return fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getStarted() {
return started;
}
public void setStarted(Date started) {
this.started = started;
}
public Date getCompleted() {
return completed;
}
public void setCompleted(Date completed) {
this.completed = completed;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
The AnalysisRepository
is, again, the basic version of a Mongo repository:
package com.msdm.analysis.repositories;
import com.msdm.analysis.entities.Analysis;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface AnalysisRepository extends MongoRepository<Analysis, String> {
}
We will also need the customary controllers, for working with a single analysis and a collection of analyses:
package com.msdm.analysis.controllers;
import com.msdm.analysis.entities.Analysis;
import com.msdm.analysis.repositories.AnalysisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
@RestController
@RequestMapping("analysis")
public class AnalysisController {
@Autowired
private AnalysisRepository analysisRepository;
@RequestMapping(method = RequestMethod.POST)
public Analysis createAnalysis(
@RequestParam("owner") String owner,
@RequestParam("fileId") String fileId
) {
Analysis analysis = new Analysis();
analysis.setOwner(owner);
analysis.setFileId(fileId);
analysis.setCreated(new Date());
Analysis savedAnalysis = analysisRepository.save(analysis);
return savedAnalysis;
}
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public Analysis getAnalysis(@PathVariable("id") String id) {
return analysisRepository.findOne(id);
}
}
package com.msdm.analysis.controllers;
import com.msdm.analysis.entities.Analysis;
import com.msdm.analysis.repositories.AnalysisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("analyses")
public class AnalysisCollectionController {
private static int PAGE = 0;
private static int SIZE = 10;
@Autowired
private AnalysisRepository analysisRepository;
@RequestMapping(
method = RequestMethod.GET,
produces = "application/json"
)
public List<Analysis> getAllAnalyses(
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "size", required = false) Integer size
) {
Pageable pageRequest = getPageRequest(page, size);
if (pageRequest != null) {
Page<Analysis> resultPage = analysisRepository.findAll(pageRequest);
return resultPage.getContent();
} else {
return analysisRepository.findAll();
}
}
private Pageable getPageRequest(Integer page, Integer size) {
if (page != null) {
if (size == null) {
size = SIZE;
}
return new PageRequest(page, size);
} else if (size != null) {
return new PageRequest(PAGE, size);
} else {
return null;
}
}
}
What is more interesting about this service is that we will need to provide an interface to communicate to the executer service which analysis is next in line:
package com.msdm.analysis.controllers;
import com.msdm.analysis.entities.Analysis;
import com.msdm.analysis.repositories.AnalysisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.Optional;
@RestController
@RequestMapping("scheduler")
public class SchedulingController {
@Autowired
private AnalysisRepository analysisRepository;
@RequestMapping(method = RequestMethod.POST)
public Analysis getNextAnalysis() {
Optional<Analysis> first = analysisRepository.findAll().stream()
.filter(a -> a.getStarted() == null)
.filter(a -> a.getCompleted() == null)
.findFirst();
if (first.isPresent()) {
Analysis analysis = first.get();
analysis.setStarted(new Date());
Analysis savedAnalysis = analysisRepository.save(analysis);
return savedAnalysis;
} else {
return null;
}
}
}
For right this moment we’ll implement a very basic scheduler. Using Java 8 streams, we’ll load all analyses in our database, filter out the ones that are already started or completed and just pick one that was not started yet. This is a very inefficient and unfair implementation of the scheduler. We can improve this by making Mongo filter out the results we need, and even add some ordering to the next analysis, for example execute analyses in the order in which they were added. These are all changes we can consider doing once we start testing out our whole system. One more thing to mention about this service is that we are using the POST method to obtain the next analysis in line. We are not submitting any data for this call, we are only receiving something, so should this not be a GET call? Now if we want to respect the idempotency of the HTTP methods as it is defined in the standards. Multiple calls to this method will alter the system state by selecting and marking an analysis as started in each call. We are choosing to use POST as a way to make it evident that there are hidden operations being performed.
We need one more API to let the executer service communicate the result of the analysis to the analysis service:
package com.msdm.analysis.controllers;
import com.msdm.analysis.entities.Analysis;
import com.msdm.analysis.repositories.AnalysisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
@RestController
@RequestMapping("result")
public class ResultController {
@Autowired
private AnalysisRepository analysisRepository;
@RequestMapping(value = "failed/{id}", method = RequestMethod.POST)
public Analysis failedAnalysis(@PathVariable("id") String id) {
Analysis analysis = analysisRepository.findOne(id);
if (analysis != null) {
analysis.setStarted(null);
Analysis savedAnalysis = analysisRepository.save(analysis);
return savedAnalysis;
} else {
return null;
}
}
@RequestMapping(value = "success/{id}", method = RequestMethod.POST)
public Analysis successAnalysis(
@PathVariable("id") String id,
@RequestParam("result") String result
) {
Analysis analysis = analysisRepository.findOne(id);
if (analysis != null) {
analysis.setCompleted(new Date());
analysis.setResult(result);
Analysis savedAnalysis = analysisRepository.save(analysis);
return savedAnalysis;
} else {
return null;
}
}
}
This interface will have two POST endpoints, on that marks an analysis as failed and adds it back to the pool of analyses that need to run, and the second one that marks the analysis as successful and saves the result of the analysis. There is much more we could add to improve the functionality of this service: some error handling and signaling, absolutely necessary to add some unit tests, and we’ll get to those later. But right now we can run it and do a minor sanity check using Postman.
The executer service will do very little, but it will be the first
service in the system that can’t function without communicating with
another service. The executer service will need to call the analysis
service to get the next analysis that it must run. After running the
analysis, it must call the analysis service again to report the result.
In between, we would also need to make a call to the file service and
download the actual data to the executer service (in case of a real
implementation). We’ll need to generate a com.msdm.executer
service with just Web
dependency, then copy this new project to our workspace. After opening
the project, we first need to make sure that the configuration is good.
We’ll need to run this service on a new port, and since this service
communicates with other services, we will need to provide a way for it
to find the other services. This will be done through properties, for
the time being:
server.port=8092
analysis.url=http://localhost:8091
analysis.endpoint.scheduler=scheduler
analysis.endpoint.failure=result/failed
analysis.endpoint.success=result/success
file.url=http://localhost:8090
file.endpoint.download=download
Next, we will need to initialize the rest template, the object we will use to make REST calls to other services. We do this by adding a method annotated with @Bean in our application configuration class, which in this case is also the starting point of the microservice:
package com.msdm.executer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableScheduling
public class ExecuterApplication {
@Bean
public RestTemplate initializeRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
// restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
return restTemplate;
}
public static void main(String[] args) {
SpringApplication.run(ExecuterApplication.class, args);
}
}
By doing this, we can configure the REST template in a single place
and use is everywhere in the application (but the default REST template
will do for now). You may notice something else there as well, the @EnableScheduling
annotation. We will come back to that later.
The we will need to add two (internal) service classes, one to handle communication with the analysis service and the other one to download the files we need to run analyses on.
package com.msdm.executer.services;
import com.msdm.executer.entities.Analysis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.net.URISyntaxException;
import java.util.List;
@Component
public class AnalysisService {
@Value("${analysis.url}")
private String analysisServiceUrl;
@Value("${analysis.endpoint.scheduler}")
private String schedulerEndpoint;
@Value("${analysis.endpoint.failure}")
private String failureEndpoint;
@Value("${analysis.endpoint.success}")
private String successEndpoint;
@Autowired
private RestTemplate restTemplate;
private String getFullUrl(String relativeURl) {
StringBuilder builder = new StringBuilder(analysisServiceUrl);
if (! analysisServiceUrl.endsWith("/")) builder.append("/");
if (relativeURl.startsWith("/")) {
builder.append(relativeURl.substring(1));
} else {
builder.append(relativeURl);
}
return builder.toString();
}
public Analysis loadNextAnalysis() {
Analysis analysis = restTemplate.postForObject(getFullUrl(schedulerEndpoint), null, Analysis.class);
return analysis;
}
public void signalFailure(String id) {
String url = getFullUrl(failureEndpoint) + "/" + id;
restTemplate.postForObject(url, null, String.class);
}
public void signalSuccess(String id, String result) throws URISyntaxException {
String url = getFullUrl(successEndpoint) + "/" + id;
MultiValueMap<String, String> parametersMap = new LinkedMultiValueMap<String, String>();
parametersMap.add("result", result);
restTemplate.postForObject(url, parametersMap, String.class);
}
}
We are loading field values with the relevant URLs to access the
analysis service from our properties file. We are also autowiring the
REST template. Our class will have three methods. The first one loads an
analysis. We need to define an entity object in our executer service so
we can save the JSON response we get from the analysis service to it.
An Analysis
entity that is
identical to the one in the analysis service will do. With the second
method, we call the analysis service to signal that the analysis has
failed. The last method signals a success and also sends the result as a
string.
package com.msdm.executer.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.net.URISyntaxException;
import java.util.Arrays;
@Component
public class FileService {
@Value("${file.url}")
private String fileServiceUrl;
@Value("${file.endpoint.download}")
private String downloadEndpoint;
@Autowired
private RestTemplate restTemplate;
public byte[] downloadFile(String id) {
String url = fileServiceUrl + "/" + downloadEndpoint + "/" + id;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));
HttpEntity<String> entity = new HttpEntity<String>(headers);
ResponseEntity<byte[]> response = restTemplate.exchange(
url,
HttpMethod.GET, entity, byte[].class);
return response.getBody();
}
}
File download is not much harder. The FileService
class has one method that downloads the file from the file service and
keeps it in memory, as a byte array. This may work for smaller files,
but if our system will need to handle large files we’ll need to find a
way to enable that. On the other hand, since files are now kept in
memory, the analysis should run very fast.
But that will not be the case, because our playground implementation needs to simulate a service that consumes a large amount of resources, and we’ll focus on making time the most used resource:
package com.msdm.executer.services;
import com.msdm.executer.entities.Analysis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ExecutorService;
@Component
public class ExecuterService {
private Logger logger = LoggerFactory.getLogger(ExecutorService.class);
@Autowired
private AnalysisService analysisService;
@Autowired
private FileService fileService;
private int chance = 1000;
private Random random = new Random(System.currentTimeMillis());
@Scheduled(fixedRate = 5000)
public void execute() {
logger.info("retrieving analysis");
Analysis analysis = analysisService.loadNextAnalysis();
if (analysis != null) {
logger.info("running analysis " + analysis.getId());
logger.info("downloading file " + analysis.getFileId());
byte[] bytes = fileService.downloadFile(analysis.getFileId());
if (bytes != null) {
logger.info("file downloaded successfully");
chance = bytes.length;
try {
String result = process(bytes);
analysisService.signalSuccess(analysis.getId(), result);
} catch (Exception e) {
logger.error(e.getMessage(), e);
analysisService.signalFailure(analysis.getId());
}
} else {
logger.info("file download failed");
logger.info("analysis failed");
analysisService.signalFailure(analysis.getId());
}
} else {
logger.info("no analysis to schedule, will try again later");
}
}
private String process(byte[] data) throws Exception {
StringBuilder builder = new StringBuilder();
for (byte b: data) {
Character character = process(b);
if (character != null) {
builder.append(character);
}
}
return builder.toString();
}
private Character process(byte b) throws Exception {
int r = random.nextInt(chance);
if (r == 0) {
throw new Exception("this analysis has failed because of " + r);
} else if (r < chance/100) {
return (char) b;
} else {
return null;
}
}
}
This is a weird service. The service is tasked with periodically running analyses. The execute()
method is annotated @Scheduled
to let Spring know it should try to run that method every 5 seconds. When we use @EnableScheduling
on the application entry class we let Spring know it should scan for @Scheduled
annotations. This all means our application will try to download and
run an analysis every five seconds. How the analysis is “run” is another
matter. All this code does is go over every byte in the dataset,
generate a random integer and do something with that byte based on the
integer. If the integer is zero, the whole analysis will fail. If the
random integer is less that 1% the size of the dataset, the byte will be
converted into a char and added to the result string. The byte is
selected from the interval 0 to size of the dataset, which means that in
a larger dataset, the chance for getting a random 0 and failing the
analysis is smaller at each step, but we have more steps than with a
small dataset. I don’t know how balanced the odds are, the main point is
the service is performing some random computation that has a chance of
failure. Later, when we test the whole system, we can add a sleep
operation in there, possibly with a random interval, that will slow this
microservice down and force use to experiment with creating multiple
instances of the service to handle a higher load to the system.
That will be all for now. I believe that at this moment we have enough code to start looking into connecting the whole system together, in the next part of the workshop.