Skip to content

Commit eccc0d0

Browse files
Eric Bottardmarkfisher
authored andcommitted
XD-1036: Enable deletion of composed modules
Record module usage Add module delete command & remove dependencies Use module delete to clean up tests Add integration tests polishing on merge - rename onAfter/onBefore to just after/before - use ConcurrentMap in InMemoryModuleDependencyRepository - changed "findDependents" to "find" in ModuleDedendencyRepository - changed "findDependents" to "find" in ModuleDependencyTracker - also added stories (see PR spring-attic#390 comments for details)
1 parent 38fa3a5 commit eccc0d0

File tree

19 files changed

+501
-57
lines changed

19 files changed

+501
-57
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.xd.dirt.module;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.xd.dirt.core.XDRuntimeException;
22+
import org.springframework.xd.module.ModuleType;
23+
24+
25+
/**
26+
* Thrown when performing an action cannot be carried over because some dependency would be broken.
27+
*
28+
* @author Eric Bottard
29+
*/
30+
@SuppressWarnings("serial")
31+
public class /* Module? */DependencyException extends XDRuntimeException {
32+
33+
private final Set<String> dependents;
34+
35+
private final String name;
36+
37+
private final ModuleType type;
38+
39+
public DependencyException(String message, String name, ModuleType type, Set<String> dependents) {
40+
super(String.format(message, name, type, dependents));
41+
this.name = name;
42+
this.type = type;
43+
this.dependents = dependents;
44+
}
45+
46+
public Set<String> getDependents() {
47+
return dependents;
48+
}
49+
50+
public String getName() {
51+
return name;
52+
}
53+
54+
public ModuleType getType() {
55+
return type;
56+
}
57+
58+
59+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.xd.dirt.module;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.xd.module.ModuleType;
22+
23+
24+
/**
25+
* Used to track usage of modules from streams and composed modules.
26+
*
27+
* @author Eric Bottard
28+
*/
29+
public interface ModuleDependencyRepository {
30+
31+
/**
32+
* Store one atomic dependency from a module (composed or not) to some target (stream, or composed module).
33+
*/
34+
void store(String moduleName, ModuleType type, /* BaseDefinition? */String target);
35+
36+
/**
37+
* Return the set of things that depend on the given module.
38+
*
39+
* @return a set of Strings of the form {@code type:name}, never {@code null}
40+
*/
41+
Set</* BaseDefinition? */String> find(String name, ModuleType type);
42+
43+
/**
44+
* Remove an atomic dependency from a module (composed or not) to some target (stream, or composed module).
45+
*/
46+
void delete(String module, ModuleType type, /* BaseDefinition? */String target);
47+
48+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.xd.dirt.module;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.xd.module.ModuleType;
23+
24+
25+
/**
26+
* Used to compute and track dependencies between modules and other elements of the system.
27+
*
28+
* @author Eric Bottard
29+
*/
30+
public class ModuleDependencyTracker {
31+
32+
private final ModuleDependencyRepository dependencyRepository;
33+
34+
@Autowired
35+
public ModuleDependencyTracker(ModuleDependencyRepository dependencyRepository) {
36+
this.dependencyRepository = dependencyRepository;
37+
}
38+
39+
public void record(ModuleDeploymentRequest source, String target) {
40+
dependencyRepository.store(source.getModule(), source.getType(), target);
41+
if (source instanceof CompositeModuleDeploymentRequest) {
42+
CompositeModuleDeploymentRequest composed = (CompositeModuleDeploymentRequest) source;
43+
for (ModuleDeploymentRequest child : composed.getChildren()) {
44+
record(child, target);
45+
}
46+
}
47+
}
48+
49+
50+
/**
51+
* Return the set of things that depend on the given module.
52+
*
53+
* @return a set of Strings of the form {@code type:name}, never {@code null}
54+
*/
55+
public Set<String> find(String name, ModuleType type) {
56+
return dependencyRepository.find(name, type);
57+
}
58+
59+
/**
60+
* @param request
61+
* @param dependencyKey
62+
*/
63+
public void remove(ModuleDeploymentRequest source, String target) {
64+
dependencyRepository.delete(source.getModule(), source.getType(), target);
65+
if (source instanceof CompositeModuleDeploymentRequest) {
66+
CompositeModuleDeploymentRequest composed = (CompositeModuleDeploymentRequest) source;
67+
for (ModuleDeploymentRequest child : composed.getChildren()) {
68+
remove(child, target);
69+
}
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.xd.dirt.module.memory;
18+
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.ConcurrentMap;
23+
24+
import org.springframework.xd.dirt.module.ModuleDependencyRepository;
25+
import org.springframework.xd.module.ModuleType;
26+
27+
28+
/**
29+
* In memory implementation of {@link ModuleDependencyRepository}.
30+
*
31+
* @author Eric Bottard
32+
*/
33+
public class InMemoryModuleDependencyRepository implements ModuleDependencyRepository {
34+
35+
private ConcurrentMap<String, Set<String>> dependencies = new ConcurrentHashMap<String, Set<String>>();
36+
37+
@Override
38+
public void store(String moduleName, ModuleType type, String target) {
39+
dependencies.putIfAbsent(keyFor(moduleName, type), new HashSet<String>());
40+
dependencies.get(keyFor(moduleName, type)).add(target);
41+
}
42+
43+
@Override
44+
public void delete(String module, ModuleType type, String target) {
45+
dependencies.get(keyFor(module, type)).remove(target);
46+
}
47+
48+
@Override
49+
public Set<String> find(String name, ModuleType type) {
50+
return dependencies.get(keyFor(name, type));
51+
}
52+
53+
private String keyFor(String moduleName, ModuleType type) {
54+
return type.name() + ":" + moduleName;
55+
}
56+
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.xd.dirt.module.redis;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.data.redis.core.BoundSetOperations;
22+
import org.springframework.data.redis.core.RedisOperations;
23+
import org.springframework.xd.dirt.module.ModuleDependencyRepository;
24+
import org.springframework.xd.module.ModuleType;
25+
26+
27+
/**
28+
* Redis implementation of {@link ModuleDependencyRepository}. Uses sets whose name is of the form
29+
* {@code dependencies.module.<type>:<name>}.
30+
*
31+
* @author Eric Bottard
32+
*/
33+
public class RedisModuleDependencyRepository implements ModuleDependencyRepository {
34+
35+
private RedisOperations<String, String> redisOperations;
36+
37+
public RedisModuleDependencyRepository(RedisOperations<String, String> redisOperations) {
38+
this.redisOperations = redisOperations;
39+
}
40+
41+
42+
@Override
43+
public void store(String moduleName, ModuleType type, String target) {
44+
setOpsFor(moduleName, type).add(target);
45+
}
46+
47+
@Override
48+
public void delete(String module, ModuleType type, String target) {
49+
setOpsFor(module, type).remove(target);
50+
}
51+
52+
@Override
53+
public Set<String> find(String name, ModuleType type) {
54+
return setOpsFor(name, type).members();
55+
}
56+
57+
private BoundSetOperations<String, String> setOpsFor(String moduleName, ModuleType type) {
58+
return redisOperations.boundSetOps(String.format("dependencies.module.%s:%s", type.name(), moduleName));
59+
}
60+
61+
}

spring-xd-dirt/src/main/java/org/springframework/xd/dirt/rest/ModulesController.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Collections;
2222
import java.util.List;
23+
import java.util.Set;
2324

2425
import org.apache.commons.logging.Log;
2526
import org.apache.commons.logging.LogFactory;
@@ -41,8 +42,10 @@
4142
import org.springframework.web.bind.annotation.RequestParam;
4243
import org.springframework.web.bind.annotation.ResponseBody;
4344
import org.springframework.web.bind.annotation.ResponseStatus;
45+
import org.springframework.xd.dirt.module.DependencyException;
4446
import org.springframework.xd.dirt.module.ModuleAlreadyExistsException;
4547
import org.springframework.xd.dirt.module.ModuleDefinitionRepository;
48+
import org.springframework.xd.dirt.module.ModuleDependencyTracker;
4649
import org.springframework.xd.dirt.module.ModuleDeploymentRequest;
4750
import org.springframework.xd.dirt.module.NoSuchModuleException;
4851
import org.springframework.xd.dirt.stream.XDStreamParser;
@@ -71,11 +74,15 @@ public class ModulesController {
7174

7275
private ModuleDefinitionResourceAssembler moduleDefinitionResourceAssembler = new ModuleDefinitionResourceAssembler();
7376

77+
private ModuleDependencyTracker dependencyTracker;
78+
7479
@Autowired
75-
public ModulesController(ModuleDefinitionRepository moduleDefinitionRepository) {
80+
public ModulesController(ModuleDefinitionRepository moduleDefinitionRepository,
81+
ModuleDependencyTracker dependencyTracker) {
7682
Assert.notNull(moduleDefinitionRepository, "moduleDefinitionRepository must not be null");
7783
this.repository = moduleDefinitionRepository;
7884
this.parser = new XDStreamParser(moduleDefinitionRepository);
85+
this.dependencyTracker = dependencyTracker;
7986
}
8087

8188
/**
@@ -120,6 +127,9 @@ public ModuleDefinitionResource save(@RequestParam("name") String name,
120127
if (repository.findByNameAndType(name, type) != null) {
121128
throw new ModuleAlreadyExistsException(name, type);
122129
}
130+
for (ModuleDeploymentRequest child : modules) {
131+
dependencyTracker.record(child, dependencyKey(name, type));
132+
}
123133

124134
ModuleDefinition moduleDefinition = new ModuleDefinition(name, type);
125135
moduleDefinition.setDefinition(definition);
@@ -128,6 +138,35 @@ public ModuleDefinitionResource save(@RequestParam("name") String name,
128138
return resource;
129139
}
130140

141+
private String dependencyKey(String name, ModuleType type) {
142+
return String.format("module:%s:%s", type.name(), name);
143+
}
144+
145+
/**
146+
* Delete a (composite) module.
147+
*/
148+
@RequestMapping(value = "/{type}/{name}", method = RequestMethod.DELETE)
149+
@ResponseStatus(HttpStatus.OK)
150+
public void delete(@PathVariable("type") ModuleType type, @PathVariable("name") String name) {
151+
ModuleDefinition definition = repository.findByNameAndType(name, type);
152+
if (definition == null) {
153+
throw new NoSuchModuleException(name, type);
154+
}
155+
if (definition.getDefinition() == null) {
156+
throw new IllegalStateException(String.format("Cannot delete non-composed module %s:%s", type, name));
157+
}
158+
Set<String> dependedUpon = dependencyTracker.find(name, type);
159+
if (!dependedUpon.isEmpty()) {
160+
throw new DependencyException("Cannot delete module %2$s:%1$s because it is used by %3$s", name, type,
161+
dependedUpon);
162+
}
163+
repository.delete(type.name() + ":" + name);
164+
List<ModuleDeploymentRequest> requests = parser.parse(name, definition.getDefinition());
165+
for (ModuleDeploymentRequest request : requests) {
166+
dependencyTracker.remove(request, dependencyKey(name, type));
167+
}
168+
}
169+
131170
private ModuleType determineType(List<ModuleDeploymentRequest> modules) {
132171
Collections.sort(modules);
133172
Assert.isTrue(modules != null && modules.size() > 0, "at least one module required");

0 commit comments

Comments
 (0)