Skip to content

Commit b359b3c

Browse files
authored
[enhancement/improveclose] Improved close feature (#49)
* [enhancement/improveclose] Improved close feature * [enhancement/improveclose] Improved code inside close to have less nesting * [enhancement/improveclose] Moved "Session died" log up a call * [enhancement/improveclose] Improved error logs in JShellService#close() * [enhancement/improveclose] Sonar
1 parent daddfe9 commit b359b3c

File tree

6 files changed

+85
-80
lines changed

6 files changed

+85
-80
lines changed

JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public List<String> snippets(@PathVariable String id,
6464
}
6565

6666
@DeleteMapping("/{id}")
67-
public void delete(@PathVariable String id) throws DockerException {
67+
public void delete(@PathVariable String id) {
6868
validateId(id);
6969
if (!service.hasSession(id))
7070
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found");

JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ public void onNext(Frame object) {
133133
public void killContainerByName(String name) {
134134
LOGGER.debug("Fetching container to kill {}.", name);
135135
List<Container> containers = client.listContainersCmd().withNameFilter(Set.of(name)).exec();
136-
LOGGER.debug("Number of containers to kill: {} for name {}.", containers.size(), name);
137-
if (containers.size() != 1) {
138-
LOGGER.error("There is more than 1 container for name {}.", name);
136+
if (containers.size() == 1) {
137+
LOGGER.debug("Found 1 container for name {}.", name);
138+
} else {
139+
LOGGER.error("Expected 1 container but found {} for name {}.", containers.size(), name);
139140
}
140141
for (Container container : containers) {
141142
client.killContainerCmd(container.getId()).exec();

JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellService.java

+54-31
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.togetherjava.jshellapi.service;
22

3-
import org.apache.tomcat.util.http.fileupload.util.Closeable;
43
import org.slf4j.Logger;
54
import org.slf4j.LoggerFactory;
65
import org.springframework.lang.Nullable;
@@ -15,13 +14,13 @@
1514
import java.util.List;
1615
import java.util.Optional;
1716

18-
public class JShellService implements Closeable {
17+
public class JShellService {
1918
private static final Logger LOGGER = LoggerFactory.getLogger(JShellService.class);
2019
private final JShellSessionService sessionService;
2120
private final String id;
2221
private final BufferedWriter writer;
2322
private final BufferedReader reader;
24-
23+
private boolean markedAsDead;
2524
private Instant lastTimeoutUpdate;
2625
private final long timeout;
2726
private final boolean renewable;
@@ -63,21 +62,20 @@ public JShellService(DockerService dockerService, JShellSessionService sessionSe
6362
startupScriptSize = Integer.parseInt(reader.readLine());
6463
} catch (Exception e) {
6564
LOGGER.warn("Unexpected error during creation.", e);
65+
markAsDead();
6666
throw new DockerException("Creation of the session failed.", e);
6767
}
6868
this.doingOperation = false;
6969
}
7070

7171
public Optional<JShellResult> eval(String code) throws DockerException {
72+
if (shouldDie())
73+
throw new DockerException("Session %s is already dead.".formatted(id));
7274
synchronized (this) {
7375
if (!tryStartOperation()) {
7476
return Optional.empty();
7577
}
7678
}
77-
if (isClosed()) {
78-
close();
79-
return Optional.empty();
80-
}
8179
updateLastTimeout();
8280
sessionService.scheduleEvalTimeoutValidation(id, evalTimeout + evalTimeoutValidationLeeway);
8381
if (!code.endsWith("\n"))
@@ -95,7 +93,7 @@ public Optional<JShellResult> eval(String code) throws DockerException {
9593
return Optional.of(readResult());
9694
} catch (DockerException | IOException | NumberFormatException ex) {
9795
LOGGER.warn("Unexpected error.", ex);
98-
close();
96+
markAsDead();
9997
throw new DockerException(ex);
10098
} finally {
10199
stopOperation();
@@ -147,6 +145,8 @@ private JShellResult readResult() throws IOException, NumberFormatException, Doc
147145
}
148146

149147
public Optional<List<String>> snippets(boolean includeStartupScript) throws DockerException {
148+
if (shouldDie())
149+
throw new DockerException("Session %s is already dead.".formatted(id));
150150
synchronized (this) {
151151
if (!tryStartOperation()) {
152152
return Optional.empty();
@@ -169,7 +169,7 @@ public Optional<List<String>> snippets(boolean includeStartupScript) throws Dock
169169
: snippets.subList(startupScriptSize, snippets.size()));
170170
} catch (Exception ex) {
171171
LOGGER.warn("Unexpected error.", ex);
172-
close();
172+
markAsDead();
173173
throw new DockerException(ex);
174174
} finally {
175175
stopOperation();
@@ -186,46 +186,70 @@ public boolean isInvalidEvalTimeout() {
186186
.isBefore(Instant.now());
187187
}
188188

189-
public boolean shouldDie() {
190-
return lastTimeoutUpdate.plusSeconds(timeout).isBefore(Instant.now());
189+
/**
190+
* Returns if this session should be killed in the next heartbeat of the session killer.
191+
*
192+
* @return true if this session should be killed in the next heartbeat of the session killer
193+
* false otherwise
194+
*/
195+
public boolean isMarkedAsDead() {
196+
return this.markedAsDead;
191197
}
192198

193-
public void stop() throws DockerException {
199+
/**
200+
* Marks this session as dead and also tries to gracefully close it, so it can be killed in the
201+
* next heartbeat of the session killer.
202+
*/
203+
public synchronized void markAsDead() {
204+
if (this.markedAsDead)
205+
return;
206+
LOGGER.info("Session {} marked as dead.", id);
207+
this.markedAsDead = true;
208+
194209
try {
195210
writer.write("exit");
196211
writer.newLine();
197212
writer.flush();
198-
} catch (IOException e) {
199-
throw new DockerException(e);
213+
} catch (IOException ex) {
214+
LOGGER.debug("Couldn't close session {} gracefully.", id, ex);
200215
}
201216
}
202217

218+
/**
219+
* Returns if this session should be killed. Returns true if either it is marked as dead, if the
220+
* timeout is reached or if the container is dead.
221+
*
222+
* @return true if this session should be killed, false otherwise
223+
*/
224+
public boolean shouldDie() {
225+
return markedAsDead || lastTimeoutUpdate.plusSeconds(timeout).isBefore(Instant.now())
226+
|| dockerService.isDead(containerName());
227+
}
228+
203229
public String id() {
204230
return id;
205231
}
206232

207-
@Override
208233
public void close() {
209-
LOGGER.debug("Close called for session {}.", id);
210234
try {
211-
dockerService.killContainerByName(containerName());
212-
try {
213-
writer.close();
214-
} finally {
215-
reader.close();
235+
writer.close();
236+
} catch (Exception ex) {
237+
LOGGER.error("Unexpected error while closing the writer.", ex);
238+
}
239+
try {
240+
reader.close();
241+
} catch (Exception ex) {
242+
LOGGER.error("Unexpected error while closing the reader.", ex);
243+
}
244+
try {
245+
if (!dockerService.isDead(containerName())) {
246+
dockerService.killContainerByName(containerName());
216247
}
217-
} catch (IOException ex) {
218-
LOGGER.error("Unexpected error while closing.", ex);
219-
} finally {
220-
sessionService.notifyDeath(id);
248+
} catch (Exception ex) {
249+
LOGGER.error("Unexpected error while destroying the container.", ex);
221250
}
222251
}
223252

224-
@Override
225-
public boolean isClosed() {
226-
return dockerService.isDead(containerName());
227-
}
228-
229253
private void updateLastTimeout() {
230254
if (renewable) {
231255
lastTimeoutUpdate = Instant.now();
@@ -243,7 +267,6 @@ private void checkContainerOK() throws DockerException {
243267
"Container of session " + id + " is dead because status was " + ok);
244268
}
245269
} catch (IOException ex) {
246-
close();
247270
throw new DockerException(ex);
248271
}
249272
}

JShellAPI/src/main/java/org/togetherjava/jshellapi/service/JShellSessionService.java

+14-31
Original file line numberDiff line numberDiff line change
@@ -28,40 +28,28 @@ public class JShellSessionService {
2828
private void initScheduler() {
2929
scheduler = Executors.newSingleThreadScheduledExecutor();
3030
scheduler.scheduleAtFixedRate(() -> {
31-
LOGGER.info("Scheduler heartbeat: started.");
32-
jshellSessions.keySet()
33-
.stream()
34-
.filter(id -> jshellSessions.get(id).isClosed())
35-
.forEach(this::notifyDeath);
3631
List<String> toDie = jshellSessions.keySet()
3732
.stream()
3833
.filter(id -> jshellSessions.get(id).shouldDie())
3934
.toList();
40-
LOGGER.info("Scheduler heartbeat: sessions ready to die: {}", toDie);
35+
LOGGER.info("Scheduler heartbeat, sessions ready to die: {}", toDie);
4136
for (String id : toDie) {
42-
try {
43-
deleteSession(id);
44-
} catch (DockerException ex) {
45-
LOGGER.error("Unexpected error when deleting session.", ex);
37+
JShellService service = jshellSessions.get(id);
38+
if (service.isMarkedAsDead()) {
39+
try {
40+
jshellSessions.remove(id).close();
41+
LOGGER.info("Session {} died.", id);
42+
} catch (Exception ex) {
43+
LOGGER.error("Unexpected exception for session {}", id, ex);
44+
}
45+
} else {
46+
service.markAsDead();
4647
}
4748
}
4849
}, config.schedulerSessionKillScanRateSeconds(),
4950
config.schedulerSessionKillScanRateSeconds(), TimeUnit.SECONDS);
5051
}
5152

52-
void notifyDeath(String id) {
53-
JShellService shellService = jshellSessions.remove(id);
54-
if (shellService == null) {
55-
LOGGER.debug("Notify death on already removed session {}.", id);
56-
return;
57-
}
58-
if (!shellService.isClosed()) {
59-
LOGGER.error("JShell Service isn't dead when it should for id {}.", id);
60-
return;
61-
}
62-
LOGGER.info("Session {} died.", id);
63-
}
64-
6553
public boolean hasSession(String id) {
6654
return jshellSessions.containsKey(id);
6755
}
@@ -85,13 +73,8 @@ public JShellService oneTimeSession(@Nullable StartupScriptId startupScriptId)
8573
true, config));
8674
}
8775

88-
public void deleteSession(String id) throws DockerException {
89-
JShellService service = jshellSessions.remove(id);
90-
try {
91-
service.stop();
92-
} finally {
93-
scheduler.schedule(service::close, 500, TimeUnit.MILLISECONDS);
94-
}
76+
public void deleteSession(String id) {
77+
jshellSessions.get(id).markAsDead();
9578
}
9679

9780
private synchronized JShellService createSession(SessionInfo sessionInfo)
@@ -128,7 +111,7 @@ public void scheduleEvalTimeoutValidation(String id, long timeSeconds) {
128111
if (service == null)
129112
return;
130113
if (service.isInvalidEvalTimeout()) {
131-
service.close();
114+
deleteSession(id);
132115
}
133116
}, timeSeconds, TimeUnit.SECONDS);
134117
}

JShellAPI/src/main/resources/application.yaml

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ jshellapi:
44
regularSessionTimeoutSeconds: 1800
55
oneTimeSessionTimeoutSeconds: 30
66
evalTimeoutSeconds: 15
7-
evalTimeoutValidationLeeway: 10
7+
evalTimeoutValidationLeeway: 5
88
sysOutCharLimit: 1024
99
maxAliveSessions: 10
1010

@@ -14,7 +14,7 @@ jshellapi:
1414
dockerCPUSetCPUs: 0
1515

1616
# Internal config
17-
schedulerSessionKillScanRateSeconds: 60
17+
schedulerSessionKillScanRateSeconds: 10
1818

1919
# Docker service config
2020
dockerResponseTimeout: 60
@@ -29,3 +29,5 @@ logging:
2929
org:
3030
springframework:
3131
web: DEBUG
32+
togetherjava:
33+
jshellapi: DEBUG

JShellWrapper/src/test/java/JShellWrapperTest.java

+8-12
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ void testHelloWorld() {
4949

5050
@Test
5151
void testExpressionResult() {
52-
evalTest(
53-
"""
54-
eval
55-
1
56-
"Hello world!\"""",
57-
"""
52+
evalTest("""
53+
eval
54+
1
55+
"Hello world!\"""", """
5856
OK
5957
0
6058
OK
@@ -67,12 +65,10 @@ void testExpressionResult() {
6765
6866
false
6967
""");
70-
evalTest(
71-
"""
72-
eval
73-
1
74-
2+2""",
75-
"""
68+
evalTest("""
69+
eval
70+
1
71+
2+2""", """
7672
OK
7773
0
7874
OK

0 commit comments

Comments
 (0)