embulk-executor-remoteserver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +42 -0
  3. data/.gitignore +14 -0
  4. data/Dockerfile +14 -0
  5. data/LICENSE +21 -0
  6. data/README.md +30 -0
  7. data/build.gradle +70 -0
  8. data/docker-compose.yml +24 -0
  9. data/docker/run_embulk_server.sh +3 -0
  10. data/gradle.properties +1 -0
  11. data/gradle/dependency-locks/compileClasspath.lockfile +40 -0
  12. data/gradle/dependency-locks/testCompileClasspath.lockfile +49 -0
  13. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  14. data/gradle/wrapper/gradle-wrapper.properties +5 -0
  15. data/gradlew +172 -0
  16. data/gradlew.bat +84 -0
  17. data/lib/embulk/executor/remoteserver.rb +3 -0
  18. data/settings.gradle +1 -0
  19. data/src/main/java/org/embulk/executor/remoteserver/EmbulkClient.java +111 -0
  20. data/src/main/java/org/embulk/executor/remoteserver/EmbulkServer.java +32 -0
  21. data/src/main/java/org/embulk/executor/remoteserver/Host.java +32 -0
  22. data/src/main/java/org/embulk/executor/remoteserver/InitializeSessionCommand.java +94 -0
  23. data/src/main/java/org/embulk/executor/remoteserver/Launcher.java +25 -0
  24. data/src/main/java/org/embulk/executor/remoteserver/NotifyTaskStateCommand.java +23 -0
  25. data/src/main/java/org/embulk/executor/remoteserver/PluginArchive.java +170 -0
  26. data/src/main/java/org/embulk/executor/remoteserver/RemoteServerExecutor.java +131 -0
  27. data/src/main/java/org/embulk/executor/remoteserver/RemoveSessionCommand.java +24 -0
  28. data/src/main/java/org/embulk/executor/remoteserver/Session.java +177 -0
  29. data/src/main/java/org/embulk/executor/remoteserver/SessionManager.java +37 -0
  30. data/src/main/java/org/embulk/executor/remoteserver/SessionState.java +143 -0
  31. data/src/main/java/org/embulk/executor/remoteserver/StartTaskCommand.java +51 -0
  32. data/src/main/java/org/embulk/executor/remoteserver/TaskExecutionException.java +11 -0
  33. data/src/main/java/org/embulk/executor/remoteserver/TaskState.java +5 -0
  34. data/src/main/java/org/embulk/executor/remoteserver/UpdateTaskStateData.java +55 -0
  35. data/src/main/resources/logback.xml +11 -0
  36. data/src/test/java/org/embulk/executor/remoteserver/TestRemoteServerExecutor.java +80 -0
  37. data/src/test/resources/json/test1.json +1 -0
  38. data/src/test/resources/json/test2.json +1 -0
  39. data/test/Gemfile +4 -0
  40. data/test/Gemfile.lock +20 -0
  41. data/test/setup.sh +8 -0
  42. metadata +119 -0
@@ -0,0 +1,131 @@
1
+ package org.embulk.executor.remoteserver;
2
+
3
+ import com.google.inject.Inject;
4
+ import org.embulk.config.Config;
5
+ import org.embulk.config.ConfigDefault;
6
+ import org.embulk.config.ConfigInject;
7
+ import org.embulk.config.ConfigSource;
8
+ import org.embulk.config.ModelManager;
9
+ import org.embulk.config.Task;
10
+ import org.embulk.exec.ForSystemConfig;
11
+ import org.embulk.spi.Exec;
12
+ import org.embulk.spi.ExecutorPlugin;
13
+ import org.embulk.spi.ProcessState;
14
+ import org.embulk.spi.ProcessTask;
15
+ import org.embulk.spi.Schema;
16
+ import org.jruby.embed.ScriptingContainer;
17
+ import org.slf4j.Logger;
18
+ import org.slf4j.LoggerFactory;
19
+
20
+ import java.io.File;
21
+ import java.io.FileOutputStream;
22
+ import java.io.IOException;
23
+ import java.io.UncheckedIOException;
24
+ import java.nio.file.Files;
25
+ import java.util.Collections;
26
+ import java.util.List;
27
+ import java.util.concurrent.TimeoutException;
28
+
29
+ public class RemoteServerExecutor implements ExecutorPlugin {
30
+ private static final Logger log = LoggerFactory.getLogger(RemoteServerExecutor.class);
31
+ private static final Host DEFAULT_HOST = new Host("localhost", 30000);
32
+ private final ConfigSource systemConfig;
33
+ private final ScriptingContainer jruby;
34
+
35
+ interface PluginTask extends Task {
36
+ @Config("hosts")
37
+ @ConfigDefault("[]")
38
+ List<Host> getHosts();
39
+
40
+ @Config("timeout_seconds")
41
+ @ConfigDefault("3600")
42
+ int getTimeoutSeconds();
43
+
44
+ @ConfigInject
45
+ ModelManager getModelManager();
46
+ }
47
+
48
+ @Inject
49
+ public RemoteServerExecutor(@ForSystemConfig ConfigSource systemConfig, ScriptingContainer jruby) {
50
+ this.systemConfig = systemConfig;
51
+ this.jruby = jruby;
52
+ }
53
+
54
+ @Override
55
+ public void transaction(ConfigSource config, Schema outputSchema, int inputTaskCount, Control control) {
56
+ PluginTask task = config.loadConfig(PluginTask.class);
57
+ if (task.getHosts().isEmpty()) {
58
+ log.info("Hosts is empty. Run with a local server.");
59
+ try (EmbulkServer _autoclosed = EmbulkServer.start(DEFAULT_HOST.getName(), DEFAULT_HOST.getPort(), 1)) {
60
+ control.transaction(outputSchema, inputTaskCount, new ExecutorImpl(inputTaskCount, task, Collections.singletonList(DEFAULT_HOST)));
61
+ } catch (IOException e) {
62
+ throw new UncheckedIOException(e);
63
+ }
64
+ } else {
65
+ control.transaction(outputSchema, inputTaskCount, new ExecutorImpl(inputTaskCount, task, task.getHosts()));
66
+ }
67
+ }
68
+
69
+ private class ExecutorImpl implements ExecutorPlugin.Executor {
70
+ private final PluginTask pluginTask;
71
+ private final int inputTaskCount;
72
+ private final List<Host> hosts;
73
+
74
+ ExecutorImpl(int inputTaskCount, PluginTask pluginTask, List<Host> hosts) {
75
+ this.inputTaskCount = inputTaskCount;
76
+ this.pluginTask = pluginTask;
77
+ this.hosts = hosts;
78
+ }
79
+
80
+ @Override
81
+ public void execute(ProcessTask processTask, ProcessState state) {
82
+ byte[] pluginArchiveBytes;
83
+ List<PluginArchive.GemSpec> gemSpecs;
84
+ try {
85
+ File tempFile = Exec.getTempFileSpace().createTempFile("gems", ".zip");
86
+ gemSpecs = archivePlugins(tempFile);
87
+ pluginArchiveBytes = Files.readAllBytes(tempFile.toPath());
88
+ } catch (IOException e) {
89
+ throw new UncheckedIOException(e);
90
+ }
91
+ // Remove 'jruby_global_bundler_plugin_source_directory' (--bundle option)
92
+ // because all gems will be loaded via PluginArchive on server
93
+ ConfigSource systemConfigToSend = systemConfig.deepCopy().remove("jruby_global_bundler_plugin_source_directory");
94
+
95
+ ModelManager modelManager = pluginTask.getModelManager();
96
+ String systemConfigJson = modelManager.writeObject(systemConfigToSend);
97
+ String pluginTaskJson = modelManager.writeObject(pluginTask);
98
+ String processTaskJson = modelManager.writeObject(processTask);
99
+
100
+ SessionState sessionState = new SessionState(
101
+ systemConfigJson, pluginTaskJson, processTaskJson, gemSpecs, pluginArchiveBytes, state, inputTaskCount, modelManager);
102
+ try (EmbulkClient client = EmbulkClient.open(sessionState, hosts)) {
103
+ client.createSession();
104
+
105
+ state.initialize(inputTaskCount, inputTaskCount);
106
+ for (int i = 0; i < inputTaskCount; i++) {
107
+ if (state.getOutputTaskState(i).isCommitted()) {
108
+ log.warn("Skipped resumed task {}", i);
109
+ continue;
110
+ }
111
+ client.startTask(i);
112
+ }
113
+ sessionState.waitUntilCompleted(pluginTask.getTimeoutSeconds() + 1); // Add 1 sec to consider network latency
114
+ } catch (InterruptedException | TimeoutException e) {
115
+ throw new IllegalStateException(e);
116
+ } catch (IOException e) {
117
+ throw new UncheckedIOException(e);
118
+ }
119
+ }
120
+
121
+ private List<PluginArchive.GemSpec> archivePlugins(File tempFile) throws IOException {
122
+ // archive plugins
123
+ PluginArchive archive = new PluginArchive.Builder()
124
+ .addLoadedRubyGems(jruby)
125
+ .build();
126
+ try (FileOutputStream fos = new FileOutputStream(tempFile)) {
127
+ return archive.dump(fos);
128
+ }
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,24 @@
1
+ package org.embulk.executor.remoteserver;
2
+
3
+ import com.github.kamatama41.nsocket.Connection;
4
+ import com.github.kamatama41.nsocket.SyncCommand;
5
+
6
+ public class RemoveSessionCommand implements SyncCommand<String, Void> {
7
+ static final String ID = "remove_session";
8
+ private final SessionManager sessionManager;
9
+
10
+ RemoveSessionCommand(SessionManager sessionManager) {
11
+ this.sessionManager = sessionManager;
12
+ }
13
+
14
+ @Override
15
+ public Void apply(String sessionId, Connection connection) {
16
+ sessionManager.removeSession(sessionId);
17
+ return null;
18
+ }
19
+
20
+ @Override
21
+ public String getId() {
22
+ return ID;
23
+ }
24
+ }
@@ -0,0 +1,177 @@
1
+ package org.embulk.executor.remoteserver;
2
+
3
+ import com.github.kamatama41.nsocket.Connection;
4
+ import org.embulk.EmbulkEmbed;
5
+ import org.embulk.config.ConfigSource;
6
+ import org.embulk.config.ModelManager;
7
+ import org.embulk.config.TaskReport;
8
+ import org.embulk.spi.Exec;
9
+ import org.embulk.spi.ExecSession;
10
+ import org.embulk.spi.ProcessTask;
11
+ import org.embulk.spi.util.Executors;
12
+ import org.jruby.embed.ScriptingContainer;
13
+ import org.slf4j.Logger;
14
+ import org.slf4j.LoggerFactory;
15
+
16
+ import java.io.ByteArrayInputStream;
17
+ import java.io.IOException;
18
+ import java.io.InputStream;
19
+ import java.io.UncheckedIOException;
20
+ import java.nio.file.Files;
21
+ import java.nio.file.Path;
22
+ import java.util.LinkedList;
23
+ import java.util.List;
24
+ import java.util.Queue;
25
+ import java.util.concurrent.ConcurrentHashMap;
26
+ import java.util.concurrent.ConcurrentMap;
27
+ import java.util.concurrent.ExecutorService;
28
+ import java.util.concurrent.TimeUnit;
29
+
30
+ import static java.nio.charset.StandardCharsets.UTF_8;
31
+
32
+ class Session implements AutoCloseable {
33
+ private static final Logger log = LoggerFactory.getLogger(Session.class);
34
+ private final String id;
35
+ private final EmbulkEmbed embed;
36
+ private final ScriptingContainer jruby;
37
+ private final RemoteServerExecutor.PluginTask pluginTask;
38
+ private final ProcessTask processTask;
39
+ private final List<PluginArchive.GemSpec> gemSpecs;
40
+ private final byte[] pluginArchive;
41
+ private final ExecSession session;
42
+ private final ModelManager modelManager;
43
+ private final ConcurrentMap<Integer, Queue<UpdateTaskStateData>> bufferMap;
44
+ private final ExecutorService sessionRunner;
45
+ private volatile Connection connection;
46
+
47
+ Session(
48
+ String id,
49
+ String systemConfig,
50
+ String pluginTaskConfig,
51
+ String processTaskConfig,
52
+ List<PluginArchive.GemSpec> gemSpecs,
53
+ byte[] pluginArchive
54
+ ) {
55
+ this.id = id;
56
+ this.embed = newEmbulkBootstrap(systemConfig).initialize();
57
+ this.jruby = embed.getInjector().getInstance(ScriptingContainer.class);
58
+ this.modelManager = embed.getModelManager();
59
+ this.pluginTask = modelManager.readObject(RemoteServerExecutor.PluginTask.class, pluginTaskConfig);
60
+ this.processTask = modelManager.readObject(ProcessTask.class, processTaskConfig);
61
+ this.gemSpecs = gemSpecs;
62
+ this.pluginArchive = pluginArchive;
63
+ this.session = ExecSession.builder(embed.getInjector()).build();
64
+ loadPluginArchive();
65
+ this.bufferMap = new ConcurrentHashMap<>();
66
+ this.sessionRunner = java.util.concurrent.Executors.newCachedThreadPool(r -> {
67
+ Thread t = new Thread(r);
68
+ t.setName("session-runner-" + id);
69
+ t.setDaemon(true);
70
+ return t;
71
+ });
72
+ }
73
+
74
+ void runTaskAsynchronously(int taskIndex) {
75
+ sessionRunner.submit(() -> Exec.doWith(session, () -> {
76
+ runTask(taskIndex);
77
+ return null;
78
+ }));
79
+ }
80
+
81
+ private void runTask(int taskIndex) throws InterruptedException {
82
+ bufferMap.putIfAbsent(taskIndex, new LinkedList<>());
83
+ try {
84
+ Executors.process(session, processTask, taskIndex, new Executors.ProcessStateCallback() {
85
+ @Override
86
+ public void started() {
87
+ sendCommand(taskIndex, new UpdateTaskStateData(id, taskIndex, TaskState.STARTED));
88
+ }
89
+
90
+ @Override
91
+ public void inputCommitted(TaskReport report) {
92
+ UpdateTaskStateData data = new UpdateTaskStateData(id, taskIndex, TaskState.INPUT_COMMITTED);
93
+ data.setTaskReport(modelManager.writeObject(report));
94
+ sendCommand(taskIndex, data);
95
+ }
96
+
97
+ @Override
98
+ public void outputCommitted(TaskReport report) {
99
+ UpdateTaskStateData data = new UpdateTaskStateData(id, taskIndex, TaskState.OUTPUT_COMMITTED);
100
+ data.setTaskReport(modelManager.writeObject(report));
101
+ sendCommand(taskIndex, data);
102
+ }
103
+ });
104
+ sendCommand(taskIndex, new UpdateTaskStateData(id, taskIndex, TaskState.FINISHED));
105
+ } catch (Exception e) {
106
+ log.warn(String.format("Failed to run task[%d]", taskIndex), e);
107
+ UpdateTaskStateData data = new UpdateTaskStateData(id, taskIndex, TaskState.FAILED);
108
+ data.setErrorMessage(e.getMessage());
109
+ sendCommand(taskIndex, data);
110
+ }
111
+
112
+ Queue<UpdateTaskStateData> buffer = bufferMap.get(taskIndex);
113
+ if (buffer.isEmpty()) {
114
+ return;
115
+ }
116
+
117
+ // Flush buffer if remaining
118
+ int waitSeconds = 10;
119
+ while (!buffer.isEmpty()) {
120
+ if (connection.isOpen()) {
121
+ flushBuffer(taskIndex, connection);
122
+ return;
123
+ }
124
+ log.warn("Connection is closed, wait {} seconds until reconnected.", waitSeconds);
125
+ TimeUnit.SECONDS.sleep(waitSeconds);
126
+ }
127
+ }
128
+
129
+ void updateConnection(Connection connection) {
130
+ this.connection = connection;
131
+ }
132
+
133
+ private void sendCommand(int taskIndex, UpdateTaskStateData data) {
134
+ bufferMap.get(taskIndex).offer(data);
135
+ if (!connection.isOpen()) {
136
+ log.warn("Connection is closed, add data to buffer.");
137
+ return;
138
+ }
139
+ flushBuffer(taskIndex, connection);
140
+ }
141
+
142
+ private void flushBuffer(int taskIndex, Connection connection) {
143
+ UpdateTaskStateData data;
144
+ Queue<UpdateTaskStateData> buffer = bufferMap.get(taskIndex);
145
+ while ((data = buffer.poll()) != null) {
146
+ connection.sendCommand(NotifyTaskStateCommand.ID, data);
147
+ }
148
+ }
149
+
150
+ private static EmbulkEmbed.Bootstrap newEmbulkBootstrap(String configJson) {
151
+ ConfigSource systemConfig = getSystemConfig(configJson);
152
+ return new EmbulkEmbed.Bootstrap().setSystemConfig(systemConfig);
153
+ }
154
+
155
+ private static ConfigSource getSystemConfig(String configJson) {
156
+ try (InputStream in = new ByteArrayInputStream(configJson.getBytes(UTF_8))) {
157
+ return EmbulkEmbed.newSystemConfigLoader().fromJson(in);
158
+ } catch (IOException e) {
159
+ throw new UncheckedIOException(e);
160
+ }
161
+ }
162
+
163
+ private void loadPluginArchive() {
164
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(pluginArchive)) {
165
+ Path gemsDir = Files.createTempDirectory("embulk_gems");
166
+ PluginArchive.load(gemsDir.toFile(), gemSpecs, bis).restoreLoadPathsTo(jruby);
167
+ } catch (IOException e) {
168
+ throw new UncheckedIOException(e);
169
+ }
170
+ }
171
+
172
+ @Override
173
+ public void close() {
174
+ log.debug("Closing the session {}", id);
175
+ sessionRunner.shutdownNow();
176
+ }
177
+ }
@@ -0,0 +1,37 @@
1
+ package org.embulk.executor.remoteserver;
2
+
3
+ import com.github.kamatama41.nsocket.Connection;
4
+
5
+ import java.util.List;
6
+ import java.util.concurrent.ConcurrentHashMap;
7
+ import java.util.concurrent.ConcurrentMap;
8
+
9
+ class SessionManager {
10
+ private final ConcurrentMap<String, Session> sessionMap;
11
+
12
+ SessionManager() {
13
+ this.sessionMap = new ConcurrentHashMap<>();
14
+ }
15
+
16
+ void registerNewSession(String sessionId,
17
+ String systemConfig,
18
+ String pluginTaskConfig,
19
+ String processTaskConfig,
20
+ List<PluginArchive.GemSpec> gemSpecs,
21
+ byte[] pluginArchive,
22
+ Connection connection) {
23
+ Session session = sessionMap.computeIfAbsent(
24
+ sessionId, (k) -> new Session(
25
+ sessionId, systemConfig, pluginTaskConfig, processTaskConfig, gemSpecs, pluginArchive));
26
+ session.updateConnection(connection);
27
+ }
28
+
29
+ Session getSession(String sessionId) {
30
+ return sessionMap.get(sessionId);
31
+ }
32
+
33
+ void removeSession(String sessionId) {
34
+ Session removed = sessionMap.remove(sessionId);
35
+ removed.close();
36
+ }
37
+ }
@@ -0,0 +1,143 @@
1
+ package org.embulk.executor.remoteserver;
2
+
3
+ import org.embulk.config.ModelManager;
4
+ import org.embulk.config.TaskReport;
5
+ import org.embulk.spi.ProcessState;
6
+ import org.slf4j.Logger;
7
+ import org.slf4j.LoggerFactory;
8
+
9
+ import java.util.List;
10
+ import java.util.Map;
11
+ import java.util.UUID;
12
+ import java.util.concurrent.ConcurrentHashMap;
13
+ import java.util.concurrent.CountDownLatch;
14
+ import java.util.concurrent.TimeUnit;
15
+ import java.util.concurrent.TimeoutException;
16
+ import java.util.stream.Collectors;
17
+
18
+ class SessionState {
19
+ private static final Logger log = LoggerFactory.getLogger(SessionState.class);
20
+
21
+ private String sessionId;
22
+ private final String systemConfigJson;
23
+ private final String pluginTaskJson;
24
+ private final String processTaskJson;
25
+ private final List<PluginArchive.GemSpec> gemSpecs;
26
+ private final byte[] pluginArchiveBytes;
27
+
28
+ private final ProcessState state;
29
+ private final CountDownLatch timer;
30
+ private final int inputTaskCount;
31
+ private final ModelManager modelManager;
32
+ private volatile boolean isFinished;
33
+ private final Map<Integer, String> errorMessages;
34
+
35
+ SessionState(
36
+ String systemConfigJson, String pluginTaskJson, String processTaskJson,
37
+ List<PluginArchive.GemSpec> gemSpecs, byte[] pluginArchiveBytes,
38
+ ProcessState state, int inputTaskCount, ModelManager modelManager) {
39
+ this.sessionId = UUID.randomUUID().toString();
40
+ this.systemConfigJson = systemConfigJson;
41
+ this.pluginTaskJson = pluginTaskJson;
42
+ this.processTaskJson = processTaskJson;
43
+ this.gemSpecs = gemSpecs;
44
+ this.pluginArchiveBytes = pluginArchiveBytes;
45
+ this.state = state;
46
+ this.timer = new CountDownLatch(inputTaskCount);
47
+ this.inputTaskCount = inputTaskCount;
48
+ this.modelManager = modelManager;
49
+ this.isFinished = false;
50
+ this.errorMessages = new ConcurrentHashMap<>();
51
+ }
52
+
53
+ String getSessionId() {
54
+ return sessionId;
55
+ }
56
+
57
+ String getSystemConfigJson() {
58
+ return systemConfigJson;
59
+ }
60
+
61
+ String getPluginTaskJson() {
62
+ return pluginTaskJson;
63
+ }
64
+
65
+ String getProcessTaskJson() {
66
+ return processTaskJson;
67
+ }
68
+
69
+ List<PluginArchive.GemSpec> getGemSpecs() {
70
+ return gemSpecs;
71
+ }
72
+
73
+ byte[] getPluginArchiveBytes() {
74
+ return pluginArchiveBytes;
75
+ }
76
+
77
+ ProcessState getState() {
78
+ return state;
79
+ }
80
+
81
+ boolean isFinished() {
82
+ return isFinished;
83
+ }
84
+
85
+ synchronized void update(UpdateTaskStateData data) {
86
+ switch (data.getTaskState()) {
87
+ case STARTED:
88
+ state.getInputTaskState(data.getTaskIndex()).start();
89
+ state.getOutputTaskState(data.getTaskIndex()).start();
90
+ break;
91
+ case INPUT_COMMITTED:
92
+ state.getInputTaskState(data.getTaskIndex()).setTaskReport(getTaskReport(data.getTaskReport()));
93
+ break;
94
+ case OUTPUT_COMMITTED:
95
+ state.getOutputTaskState(data.getTaskIndex()).setTaskReport(getTaskReport(data.getTaskReport()));
96
+ break;
97
+ case FAILED:
98
+ errorMessages.put(data.getTaskIndex(), data.getErrorMessage());
99
+ timer.countDown();
100
+ break;
101
+ case FINISHED:
102
+ state.getInputTaskState(data.getTaskIndex()).finish();
103
+ state.getOutputTaskState(data.getTaskIndex()).finish();
104
+ timer.countDown();
105
+ showProgress(state, inputTaskCount);
106
+ break;
107
+ }
108
+ }
109
+
110
+ void waitUntilCompleted(int timeoutSeconds) throws InterruptedException, TimeoutException {
111
+ try {
112
+ if (!timer.await(timeoutSeconds, TimeUnit.SECONDS)) {
113
+ throw new TimeoutException(String.format("The session (%s) was time-out.", sessionId));
114
+ }
115
+ if (!errorMessages.isEmpty()) {
116
+ String message = errorMessages.entrySet().stream()
117
+ .map(e -> String.format("%d: %s", e.getKey(), e.getValue()))
118
+ .collect(Collectors.joining(System.lineSeparator()));
119
+ throw new TaskExecutionException(message);
120
+ }
121
+ } finally {
122
+ isFinished = true;
123
+ }
124
+ }
125
+
126
+ private TaskReport getTaskReport(String json) {
127
+ return modelManager.readObject(TaskReport.class, json);
128
+ }
129
+
130
+ private static void showProgress(ProcessState state, int taskCount) {
131
+ int started = 0;
132
+ int finished = 0;
133
+ for (int i = 0; i < taskCount; i++) {
134
+ if (state.getOutputTaskState(i).isStarted()) {
135
+ started++;
136
+ }
137
+ if (state.getOutputTaskState(i).isFinished()) {
138
+ finished++;
139
+ }
140
+ }
141
+ log.info(String.format("{done:%3d / %d, running: %d}", finished, taskCount, started - finished));
142
+ }
143
+ }