-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathJShellSessionService.java
147 lines (131 loc) · 5.71 KB
/
JShellSessionService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package org.togetherjava.jshellapi.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import org.togetherjava.jshellapi.Config;
import org.togetherjava.jshellapi.exceptions.DockerException;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
public class JShellSessionService {
private static final Logger LOGGER = LoggerFactory.getLogger(JShellSessionService.class);
private Config config;
private StartupScriptsService startupScriptsService;
private ScheduledExecutorService scheduler;
private DockerService dockerService;
private final Map<String, JShellService> jshellSessions = new HashMap<>();
private void initScheduler() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
LOGGER.info("Scheduler heartbeat: started.");
jshellSessions.keySet()
.stream()
.filter(id -> jshellSessions.get(id).isClosed())
.forEach(this::notifyDeath);
List<String> toDie = jshellSessions.keySet()
.stream()
.filter(id -> jshellSessions.get(id).shouldDie())
.toList();
LOGGER.info("Scheduler heartbeat: sessions ready to die: {}", toDie);
for (String id : toDie) {
try {
deleteSession(id);
} catch (DockerException ex) {
LOGGER.error("Unexpected error when deleting session.", ex);
}
}
}, config.schedulerSessionKillScanRateSeconds(),
config.schedulerSessionKillScanRateSeconds(), TimeUnit.SECONDS);
}
void notifyDeath(String id) {
JShellService shellService = jshellSessions.remove(id);
if (shellService == null) {
LOGGER.debug("Notify death on already removed session {}.", id);
return;
}
if (!shellService.isClosed()) {
LOGGER.error("JShell Service isn't dead when it should for id {}.", id);
}
LOGGER.info("Session {} died.", id);
}
public boolean hasSession(String id) {
return jshellSessions.containsKey(id);
}
public JShellService session(String id, @Nullable StartupScriptId startupScriptId)
throws DockerException {
if (!hasSession(id)) {
return createSession(new SessionInfo(id, true, startupScriptId, false, config));
}
return jshellSessions.get(id);
}
public JShellService session(@Nullable StartupScriptId startupScriptId) throws DockerException {
return createSession(new SessionInfo(UUID.randomUUID().toString(), false, startupScriptId,
false, config));
}
public JShellService oneTimeSession(@Nullable StartupScriptId startupScriptId)
throws DockerException {
return createSession(new SessionInfo(UUID.randomUUID().toString(), false, startupScriptId,
true, config));
}
public void deleteSession(String id) throws DockerException {
JShellService service = jshellSessions.remove(id);
service.stop();
scheduler.schedule(service::close, 500, TimeUnit.MILLISECONDS);
}
private synchronized JShellService createSession(SessionInfo sessionInfo)
throws DockerException {
// Just in case race condition happens just before createSession
if (hasSession(sessionInfo.id())) {
return jshellSessions.get(sessionInfo.id());
}
if (jshellSessions.size() >= config.maxAliveSessions()) {
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS,
"Too many sessions, try again later :(.");
}
LOGGER.info("Creating session : {}.", sessionInfo);
JShellService service = new JShellService(dockerService, this, sessionInfo.id(),
sessionInfo.sessionTimeout(), sessionInfo.renewable(), sessionInfo.evalTimeout(),
sessionInfo.evalTimeoutValidationLeeway(), sessionInfo.sysOutCharLimit(),
config.dockerMaxRamMegaBytes(), config.dockerCPUsUsage(), config.dockerCPUSetCPUs(),
startupScriptsService.get(sessionInfo.startupScriptId()));
jshellSessions.put(sessionInfo.id(), service);
return service;
}
/**
* Schedule the validation of the session timeout. In case the code runs for too long, checks if
* the wrapper correctly followed the eval timeout and canceled it, if it didn't, forcefully
* close the session.
*
* @param id the id of the session
* @param timeSeconds the time to schedule
*/
public void scheduleEvalTimeoutValidation(String id, long timeSeconds) {
scheduler.schedule(() -> {
JShellService service = jshellSessions.get(id);
if (service == null)
return;
if (service.isInvalidEvalTimeout()) {
service.close();
}
}, timeSeconds, TimeUnit.SECONDS);
}
@Autowired
public void setConfig(Config config) {
this.config = config;
initScheduler();
}
@Autowired
public void setStartupScriptsService(StartupScriptsService startupScriptsService) {
this.startupScriptsService = startupScriptsService;
}
@Autowired
public void setDockerService(DockerService dockerService) {
this.dockerService = dockerService;
}
}