Skip to content

Commit

Permalink
Added BE validation when creating or updating dependency #54
Browse files Browse the repository at this point in the history
1. For circular dependencies
2. For dependency with same source and target (self cycle)
3. For invalid situations / inputs
4. Unit tests for all the stuff
  • Loading branch information
tillias committed Oct 26, 2020
1 parent ba8ccae commit 58af8bd
Show file tree
Hide file tree
Showing 11 changed files with 691 additions and 215 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@
<artifactId>jgrapht-core</artifactId>
<version>${jgrapht.version}</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-io</artifactId>
<version>${jgrapht.version}</version>
</dependency>
<dependency>
<groupId>io.github.jhipster</groupId>
<artifactId>jhipster-framework</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.github.microcatalog.service.custom;

import com.github.microcatalog.domain.Dependency;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.service.custom.exceptions.CircularDependenciesException;
import com.github.microcatalog.service.custom.exceptions.SelfCircularException;
import com.github.microcatalog.web.rest.errors.BadRequestAlertException;
import org.jgrapht.Graph;
import org.jgrapht.alg.cycle.CycleDetector;
import org.jgrapht.graph.DefaultEdge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

@Service
@Transactional
public class DependencyService {

private final Logger log = LoggerFactory.getLogger(DependencyService.class);

private final GraphLoaderService graphLoaderService;
private final DependencyRepository repository;

public DependencyService(GraphLoaderService graphLoaderService, DependencyRepository repository) {
this.graphLoaderService = graphLoaderService;
this.repository = repository;
}

public Dependency create(final Dependency dependency) {
if (dependency.getId() != null) {
throw new BadRequestAlertException("A new dependency cannot already have an ID", getEntityName(), "idexists");
}

validateSelfCycle(dependency);
validateIfAdded(dependency);

return repository.save(dependency);
}

public Dependency update(final Dependency dependency) {
if (dependency.getId() == null) {
throw new BadRequestAlertException("Invalid id", getEntityName(), "idnull");
}

validateSelfCycle(dependency);

// TODO
// FIXME
// We need second method which updates existing instead of adding new one!
validateIfUpdated(dependency);

return repository.save(dependency);
}

public List<Dependency> findAll() {
return repository.findAll();
}

public Optional<Dependency> findById(Long id) {
return repository.findById(id);
}

public void deleteById(Long id) {
repository.deleteById(id);
}

private String getEntityName() {
return Dependency.class.getSimpleName().toLowerCase();
}

private void validateSelfCycle(final Dependency dependency) {
if (dependency == null) {
return;
}

if (dependency.getSource() == null || dependency.getTarget() == null) {
return;
}

final Long sourceId = dependency.getSource().getId();
if (Objects.equals(sourceId, dependency.getTarget().getId())) {
throw new SelfCircularException(String.format("Source id is the same as target id: %s", sourceId));
}
}

private void validateIfAdded(final Dependency toBeAdded) {
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();
graph.addEdge(toBeAdded.getSource(), toBeAdded.getTarget());

checkCycles(graph);
}

private void validateIfUpdated(final Dependency dependency) {
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();
final Dependency persistent = repository.findById(dependency.getId())
.orElseThrow(() -> new IllegalArgumentException("Trying to update dependency, but it was removed from data source"));

// check what will be if we remove existing edge and replace it with updated one

final DefaultEdge currentEdge = graph.getEdge(persistent.getSource(), persistent.getTarget());
graph.removeEdge(currentEdge);

graph.addEdge(dependency.getSource(), dependency.getTarget());

checkCycles(graph);
}

private void checkCycles(final Graph<Microservice, DefaultEdge> graph) {
final CycleDetector<Microservice, DefaultEdge> cycleDetector = new CycleDetector<>(graph);
if (cycleDetector.detectCycles()) {
final Set<Microservice> cycles = cycleDetector.findCycles();
throw new CircularDependenciesException("Circular dependency will be introduced.", cycles);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.microcatalog.service.custom;

import com.github.microcatalog.domain.Dependency;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.repository.MicroserviceRepository;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Service
@Transactional
public class GraphLoaderService {

private final MicroserviceRepository microserviceRepository;
private final DependencyRepository dependencyRepository;

public GraphLoaderService(MicroserviceRepository microserviceRepository, DependencyRepository dependencyRepository) {
this.microserviceRepository = microserviceRepository;
this.dependencyRepository = dependencyRepository;
}

public Graph<Microservice, DefaultEdge> loadGraph() {
List<Microservice> microservices = microserviceRepository.findAll();
List<Dependency> dependencies = dependencyRepository.findAll();

return createGraph(microservices, dependencies);
}

private Graph<Microservice, DefaultEdge> createGraph(final List<Microservice> nodes, final List<Dependency> edges) {
final Graph<Microservice, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);

nodes.forEach(graph::addVertex);
edges.forEach(d -> graph.addEdge(d.getSource(), d.getTarget()));

return graph;
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package com.github.microcatalog.service.custom;

import com.github.microcatalog.domain.*;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.domain.custom.ReleaseGroup;
import com.github.microcatalog.domain.custom.ReleasePath;
import com.github.microcatalog.domain.custom.ReleaseStep;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.repository.MicroserviceRepository;
import org.jgrapht.Graph;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.alg.cycle.CycleDetector;
import org.jgrapht.graph.AsSubgraph;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.EdgeReversedGraph;
import org.jgrapht.nio.dot.DOTExporter;
import org.jgrapht.traverse.DepthFirstIterator;
import org.jgrapht.traverse.GraphIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.io.StringWriter;
import java.io.Writer;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
Expand All @@ -33,79 +33,67 @@ public class ReleasePathCustomService {

private final Logger log = LoggerFactory.getLogger(ReleasePathCustomService.class);

private final MicroserviceRepository microserviceRepository;
private final DependencyRepository dependencyRepository;
private final GraphLoaderService graphLoaderService;

public ReleasePathCustomService(MicroserviceRepository microserviceRepository,
DependencyRepository dependencyRepository) {
this.microserviceRepository = microserviceRepository;
this.dependencyRepository = dependencyRepository;
public ReleasePathCustomService(GraphLoaderService graphLoaderService) {
this.graphLoaderService = graphLoaderService;
}


public Optional<ReleasePath> getReleasePath(final Long microserviceId) {
final List<Microservice> microservices = microserviceRepository.findAll();
final List<Dependency> dependencies = dependencyRepository.findAll();
final Optional<Microservice> maybeTarget =
microservices.stream().filter(m -> Objects.equals(m.getId(), microserviceId)).findFirst();
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();
final Microservice target = new Microservice();
target.setId(microserviceId);

if (!maybeTarget.isPresent()) {
// can't build release path, cause microservice with given id is not present in graph
if (!graph.containsVertex(target)) {
return Optional.empty();
}

final Graph<Long, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);

microservices.forEach(m -> graph.addVertex(m.getId()));
dependencies.forEach(d -> graph.addEdge(d.getSource().getId(), d.getTarget().getId()));

final ConnectivityInspector<Long, DefaultEdge> inspector = new ConnectivityInspector<>(graph);
final Microservice target = maybeTarget.get();
final Set<Long> connectedSet = inspector.connectedSetOf(target.getId());
final ConnectivityInspector<Microservice, DefaultEdge> inspector = new ConnectivityInspector<>(graph);
final Set<Microservice> connectedSet = inspector.connectedSetOf(target);

// Connected subgraph, that contains target microservice
final AsSubgraph<Long, DefaultEdge> targetSubgraph = new AsSubgraph<>(graph, connectedSet);
final AsSubgraph<Microservice, DefaultEdge> targetSubgraph = new AsSubgraph<>(graph, connectedSet);
log.debug("Connected subgraph, that contains target microservice: {}", targetSubgraph);

final CycleDetector<Long, DefaultEdge> cycleDetector = new CycleDetector<>(targetSubgraph);
final CycleDetector<Microservice, DefaultEdge> cycleDetector = new CycleDetector<>(targetSubgraph);
if (cycleDetector.detectCycles()) {
final Set<Long> cycles = cycleDetector.findCycles();
final Set<Microservice> cycles = cycleDetector.findCycles();
throw new IllegalArgumentException(String.format("There are cyclic dependencies between microservices : %s", cycles));
}

final Set<Long> pathMicroservices = new HashSet<>();
GraphIterator<Long, DefaultEdge> iterator = new DepthFirstIterator<>(targetSubgraph, target.getId());
final Set<Microservice> pathMicroservices = new HashSet<>();
GraphIterator<Microservice, DefaultEdge> iterator = new DepthFirstIterator<>(targetSubgraph, target);
while (iterator.hasNext()) {
pathMicroservices.add(iterator.next());
}

// TODO new use-case and visualisation
// For each element of pathSet calculate all nodes who depends on items from pathSet. Microservices which will be possibly affected if we build target and it's direct dependencies

final Graph<Long, DefaultEdge> pathGraph = new AsSubgraph<>(targetSubgraph, pathMicroservices);
final Graph<Microservice, DefaultEdge> pathGraph = new AsSubgraph<>(targetSubgraph, pathMicroservices);
log.debug("Connected subgraph, which contains all paths from target microservice to it's dependencies {}", pathGraph);

final Graph<Long, DefaultEdge> reversed = new EdgeReversedGraph<>(pathGraph);
final Graph<Long, DefaultEdge> reversedCopy = new AsSubgraph<>(reversed);

return Optional.of(convert(reversedCopy, microservices, target));
final Graph<Microservice, DefaultEdge> reversed = new EdgeReversedGraph<>(pathGraph);
return Optional.of(convert(reversed, target));
}

private ReleasePath convert(final Graph<Long, DefaultEdge> graph, final List<Microservice> microservices, final Microservice target) {
private ReleasePath convert(final Graph<Microservice, DefaultEdge> graph, final Microservice target) {
final ReleasePath result = new ReleasePath();
result.setCreatedOn(Instant.now());
result.setTarget(target);

final List<ReleaseGroup> groups = new ArrayList<>();
final Map<Long, Microservice> microserviceMap = microservices.stream()
.collect(Collectors.toMap(Microservice::getId, m -> m));

do {
final List<Long> verticesWithoutIncomingEdges = graph.vertexSet().stream()
final List<Microservice> verticesWithoutIncomingEdges = graph.vertexSet().stream()
.filter(v -> graph.incomingEdgesOf(v).isEmpty())
.collect(Collectors.toList());
log.debug("Leaves: {}", verticesWithoutIncomingEdges);

final ReleaseGroup group = new ReleaseGroup();
group.setSteps(convertSteps(microserviceMap, verticesWithoutIncomingEdges, graph));
group.setSteps(convertSteps(verticesWithoutIncomingEdges, graph));
groups.add(group);

verticesWithoutIncomingEdges.forEach(graph::removeVertex);
Expand All @@ -116,23 +104,22 @@ private ReleasePath convert(final Graph<Long, DefaultEdge> graph, final List<Mic
return result;
}

private Set<ReleaseStep> convertSteps(final Map<Long, Microservice> microserviceMap,
final List<Long> microserviceIds,
final Graph<Long, DefaultEdge> graph) {
private Set<ReleaseStep> convertSteps(final List<Microservice> verticesWithoutIncomingEdges,
final Graph<Microservice, DefaultEdge> graph) {
final Set<ReleaseStep> result = new HashSet<>();

microserviceIds.forEach(id -> {
verticesWithoutIncomingEdges.forEach(microservice -> {
final List<Microservice> parentWorkItems = new ArrayList<>();

final Set<DefaultEdge> outgoingEdges = graph.outgoingEdgesOf(id);
final Set<DefaultEdge> outgoingEdges = graph.outgoingEdgesOf(microservice);
for (DefaultEdge e : outgoingEdges) {
final Long edgeTarget = graph.getEdgeTarget(e);
parentWorkItems.add(microserviceMap.get(edgeTarget));
final Microservice edgeTarget = graph.getEdgeTarget(e);
parentWorkItems.add(edgeTarget);
}

result.add(
new ReleaseStep()
.workItem(microserviceMap.get(id))
.workItem(microservice)
.parentWorkItems(parentWorkItems)
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.microcatalog.service.custom.exceptions;

import com.github.microcatalog.domain.Microservice;

import java.util.Set;

/**
* Occurs when adding new dependency will introduce cycle
*/
public class CircularDependenciesException extends RuntimeException {
private final Set<Microservice> cycles;

public CircularDependenciesException(String message, Set<Microservice> cycles) {
super(message);

this.cycles = cycles;
}

public Set<Microservice> getCycles() {
return cycles;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.microcatalog.service.custom.exceptions;

/**
* Occurs when there is attempt creating dependency with source and target pointing to the same microservice
*/
public class SelfCircularException extends RuntimeException {
public SelfCircularException(String message) {
super(message);
}
}
Loading

0 comments on commit 58af8bd

Please sign in to comment.