• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / JavaSpiNNaker / 6310285782

26 Sep 2023 08:47AM UTC coverage: 36.367% (-0.5%) from 36.866%
6310285782

Pull #658

github

dkfellows
Merge branch 'master' into java-17
Pull Request #658: Update Java version to 17 and JEE to 9

1675 of 1675 new or added lines in 266 files covered. (100.0%)

8368 of 23010 relevant lines covered (36.37%)

0.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.03
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/compat/V1CompatTask.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
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
 *     https://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
package uk.ac.manchester.spinnaker.alloc.compat;
17

18
import static java.lang.Thread.currentThread;
19
import static java.lang.Thread.interrupted;
20
import static java.nio.charset.StandardCharsets.UTF_8;
21
import static java.util.Objects.isNull;
22
import static java.util.Objects.nonNull;
23
import static java.util.Objects.requireNonNull;
24
import static org.apache.commons.io.IOUtils.buffer;
25
import static org.slf4j.LoggerFactory.getLogger;
26
import static uk.ac.manchester.spinnaker.alloc.compat.Utils.parseDec;
27
import static uk.ac.manchester.spinnaker.alloc.model.PowerState.OFF;
28
import static uk.ac.manchester.spinnaker.alloc.model.PowerState.ON;
29

30
import java.io.BufferedReader;
31
import java.io.IOException;
32
import java.io.InputStreamReader;
33
import java.io.InterruptedIOException;
34
import java.io.OutputStreamWriter;
35
import java.io.Reader;
36
import java.io.Writer;
37
import java.net.InetSocketAddress;
38
import java.net.Socket;
39
import java.net.SocketException;
40
import java.net.SocketTimeoutException;
41
import java.util.List;
42
import java.util.Map;
43
import java.util.Objects;
44
import java.util.Optional;
45

46
import org.slf4j.Logger;
47

48
import com.fasterxml.jackson.core.JsonParseException;
49
import com.fasterxml.jackson.databind.JsonMappingException;
50
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
51
import com.google.errorprone.annotations.concurrent.GuardedBy;
52

53
import jakarta.validation.Valid;
54
import jakarta.validation.constraints.NotBlank;
55
import jakarta.validation.constraints.NotNull;
56
import jakarta.validation.constraints.Positive;
57
import uk.ac.manchester.spinnaker.alloc.model.PowerState;
58
import uk.ac.manchester.spinnaker.machine.ValidP;
59
import uk.ac.manchester.spinnaker.machine.ValidX;
60
import uk.ac.manchester.spinnaker.machine.ValidY;
61
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
62
import uk.ac.manchester.spinnaker.machine.board.ValidBoardNumber;
63
import uk.ac.manchester.spinnaker.machine.board.ValidCabinetNumber;
64
import uk.ac.manchester.spinnaker.machine.board.ValidFrameNumber;
65
import uk.ac.manchester.spinnaker.machine.board.ValidTriadHeight;
66
import uk.ac.manchester.spinnaker.machine.board.ValidTriadWidth;
67
import uk.ac.manchester.spinnaker.machine.board.ValidTriadX;
68
import uk.ac.manchester.spinnaker.machine.board.ValidTriadY;
69
import uk.ac.manchester.spinnaker.machine.board.ValidTriadZ;
70
import uk.ac.manchester.spinnaker.spalloc.messages.BoardCoordinates;
71
import uk.ac.manchester.spinnaker.spalloc.messages.BoardPhysicalCoordinates;
72
import uk.ac.manchester.spinnaker.spalloc.messages.JobDescription;
73
import uk.ac.manchester.spinnaker.spalloc.messages.JobMachineInfo;
74
import uk.ac.manchester.spinnaker.spalloc.messages.JobState;
75
import uk.ac.manchester.spinnaker.spalloc.messages.Machine;
76
import uk.ac.manchester.spinnaker.spalloc.messages.WhereIs;
77
import uk.ac.manchester.spinnaker.utils.validation.IPAddress;
78

79
/**
80
 * The core of tasks that handle connections by clients.
81
 *
82
 * @author Donal Fellows
83
 */
84
public abstract class V1CompatTask extends V1CompatService.Aware {
85
        private static final Logger log = getLogger(V1CompatTask.class);
1✔
86

87
        private static final int TRIAD_COORD_COUNT = 3;
88

89
        /**
90
         * The socket that this task is handling.
91
         */
92
        private final Socket sock;
93

94
        /**
95
         * How to read from the socket. The protocol expects messages to be UTF-8
96
         * lines, with each line being a JSON document.
97
         */
98
        private final BufferedReader in;
99

100
        /**
101
         * How to write to the socket. The protocol expects messages to be UTF-8
102
         * lines, with each line being a JSON document.
103
         * <p>
104
         * Note that synchronisation will be performed on this object.
105
         */
106
        @GuardedBy("itself")
107
        private final Writer out;
108

109
        /**
110
         * Make an instance that wraps a socket.
111
         *
112
         * @param srv
113
         *            The overall service, used for looking up shared resources that
114
         *            are uncomfortable as beans.
115
         * @param sock
116
         *            The socket that talks to the client.
117
         * @throws IOException
118
         *             If access to the socket fails.
119
         */
120
        protected V1CompatTask(V1CompatService srv, Socket sock)
121
                        throws IOException {
122
                super(srv);
×
123
                this.sock = sock;
×
124
                sock.setTcpNoDelay(true);
×
125
                sock.setSoTimeout((int) getProperties().getReceiveTimeout().toMillis());
×
126

127
                in = buffer(new InputStreamReader(sock.getInputStream(), UTF_8));
×
128
                out = new OutputStreamWriter(sock.getOutputStream(), UTF_8);
×
129
        }
×
130

131
        /**
132
         * Constructor for testing. Makes a task that isn't connected to a socket.
133
         *
134
         * @param srv
135
         *            The overall service, used for looking up shared resources that
136
         *            are uncomfortable as beans.
137
         * @param in
138
         *            Input to the task.
139
         * @param out
140
         *            Output to the task.
141
         */
142
        protected V1CompatTask(V1CompatService srv, Reader in, Writer out) {
143
                super(srv);
1✔
144
                this.sock = null;
1✔
145
                this.in = buffer(in);
1✔
146
                this.out = out;
1✔
147
        }
1✔
148

149
        final void handleConnection() {
150
                log.debug("waiting for commands from {}", sock);
1✔
151
                try {
152
                        while (!interrupted()) {
1✔
153
                                if (!communicate()) {
1✔
154
                                        log.debug("Communcation break");
1✔
155
                                        break;
1✔
156
                                }
157
                        }
158
                        if (interrupted()) {
1✔
159
                                log.debug("Shutdown on interrupt");
×
160
                        }
161
                } catch (InterruptedException | InterruptedIOException interrupted) {
×
162
                        log.debug("interrupted", interrupted);
×
163
                } catch (IOException e) {
×
164
                        if (!e.getMessage().equals("Pipe closed")) {
×
165
                                log.error("problem with socket {}", sock, e);
×
166
                        }
167
                } finally {
168
                        shutdown();
1✔
169
                }
170
        }
1✔
171

172
        private void shutdown() {
173
                var origin = nonNull(sock) ? sock : "TEST";
1✔
174
                log.debug("closing down connection from {}", origin);
1✔
175
                closeNotifiers();
1✔
176
                try {
177
                        if (nonNull(sock)) {
1✔
178
                                sock.close();
×
179
                        } else {
180
                                in.close();
1✔
181
                                synchronized (out) {
1✔
182
                                        out.close();
1✔
183
                                }
1✔
184
                        }
185
                } catch (IOException e) {
×
186
                        log.error("problem closing connection {}", origin, e);
×
187
                }
1✔
188
                log.debug("closed connection from {}", origin);
1✔
189
        }
1✔
190

191
        /**
192
         * Stop any current running notifiers.
193
         */
194
        protected abstract void closeNotifiers();
195

196
        /**
197
         * What host is connected to this service instance?
198
         *
199
         * @return The remote host that this task is serving.
200
         */
201
        public final String host() {
202
                if (isNull(sock)) {
1✔
203
                        return "<NOWHERE>";
1✔
204
                }
205
                return ((InetSocketAddress) sock.getRemoteSocketAddress()).getAddress()
×
206
                                .getHostAddress();
×
207
        }
208

209
        /**
210
         * Parse a command from a message.
211
         *
212
         * @param msg
213
         *            The message to parse.
214
         * @return The command.
215
         * @throws IOException
216
         *             If the message doesn't contain a valid command.
217
         */
218
        protected Command parseCommand(String msg) throws IOException {
219
                return getJsonMapper().readValue(msg, Command.class);
1✔
220
        }
221

222
        /**
223
         * Parse a command that was saved in the DB.
224
         *
225
         * @param msg
226
         *            The saved command to parse.
227
         * @return The command, or {@code null} if the message can't be parsed.
228
         */
229
        protected Command parseCommand(byte[] msg) {
230
                if (isNull(msg)) {
1✔
231
                        return null;
×
232
                }
233
                try {
234
                        return getJsonMapper().readValue(msg, Command.class);
1✔
235
                } catch (IOException e) {
×
236
                        log.error("unexpected failure parsing JSON", e);
×
237
                        return null;
×
238
                }
239
        }
240

241
        /**
242
         * Read a command message from the client. The message will have occupied
243
         * one line of text on the input stream from the socket.
244
         *
245
         * @return The parsed command message, or {@code empty} on end-of-stream.
246
         * @throws IOException
247
         *             If things go wrong, such as a bad or incomplete message.
248
         * @throws InterruptedException
249
         *             If interrupted.
250
         */
251
        private Optional<Command> readMessage()
252
                        throws IOException, InterruptedException {
253
                String line;
254
                try {
255
                        line = in.readLine();
1✔
256
                        log.debug("Incoming message: {}", line);
1✔
257
                } catch (SocketException e) {
×
258
                        /*
259
                         * Don't know why we get a generic socket exception for some of
260
                         * these, but it happens when there's been some sort of network drop
261
                         * or if the connection close happens in a weird order. Treating as
262
                         * EOF is the right thing.
263
                         *
264
                         * I also don't know why there is no nicer way of detecting this
265
                         * than matching the exception message. You'd think that you'd get
266
                         * something better, but no...
267
                         */
268
                        return switch (e.getMessage()) {
×
269
                        case "Connection reset", "Connection timed out (Read failed)" ->
270
                                Optional.empty();
×
271
                        default -> throw e;
×
272
                        };
273
                } catch (InterruptedIOException e) {
×
274
                        var ex = new InterruptedException();
×
275
                        ex.initCause(e);
×
276
                        throw ex;
×
277
                }
1✔
278
                if (currentThread().isInterrupted()) {
1✔
279
                        throw new InterruptedException();
×
280
                }
281
                if (currentThread().isInterrupted()) {
1✔
282
                        throw new InterruptedException();
×
283
                }
284
                if (isNull(line) || line.isBlank()) {
1✔
285
                        return Optional.empty();
1✔
286
                }
287
                var c = parseCommand(line);
1✔
288
                if (isNull(c) || isNull(c.getCommand())) {
1✔
289
                        throw new IOException("message did not specify a command");
×
290
                }
291
                log.debug("Command: {}", c);
1✔
292
                return Optional.of(c);
1✔
293
        }
294

295
        /**
296
         * Basic message send. Synchronised so that only full messages are written.
297
         *
298
         * @param msg
299
         *            The message to send. Must serializable to JSON.
300
         * @throws IOException
301
         *             If the message can't be written.
302
         */
303
        private void sendMessage(Object msg) throws IOException {
304
                // We go via a string to avoid early closing issues
305
                var data = getJsonMapper().writeValueAsString(msg);
1✔
306
                log.debug("about to send message: {}", data);
1✔
307
                // Synch so we definitely don't interleave bits of messages
308
                synchronized (out) {
1✔
309
                        out.write(data + "\r\n");
1✔
310
                        out.flush();
1✔
311
                }
1✔
312
        }
1✔
313

314
        private boolean mayWrite() {
315
                if (isNull(sock)) {
1✔
316
                        return true;
1✔
317
                }
318
                return !sock.isClosed();
×
319
        }
320

321
        /**
322
         * Send a response message.
323
         *
324
         * @param response
325
         *            The body object of the response.
326
         * @throws IOException
327
         *             If network access fails, or the object isn't serializable as
328
         *             JSON or a suitable primitive.
329
         */
330
        protected final void writeResponse(Object response) throws IOException {
331
                if (mayWrite()) {
1✔
332
                        sendMessage(new ReturnResponse(response));
1✔
333
                }
334
        }
1✔
335

336
        /**
337
         * Send an exception message.
338
         *
339
         * @param exn
340
         *            A description of the exception.
341
         * @throws IOException
342
         *             If network access fails.
343
         */
344
        protected final void writeException(Throwable exn) throws IOException {
345
                if (mayWrite()) {
1✔
346
                        if (nonNull(exn.getMessage())) {
1✔
347
                                sendMessage(new ExceptionResponse(exn.getMessage()));
1✔
348
                        } else {
349
                                sendMessage(new ExceptionResponse(exn.toString()));
×
350
                        }
351
                }
352
        }
1✔
353

354
        /**
355
         * Send a notification about a collection of jobs changing.
356
         *
357
         * @param jobIds
358
         *            The jobs that have changed. (Usually <em>all</em> jobs.)
359
         * @throws IOException
360
         *             If network access fails.
361
         */
362
        protected final void writeJobNotification(List<Integer> jobIds)
363
                        throws IOException {
364
                if (!jobIds.isEmpty() && mayWrite()) {
×
365
                        sendMessage(new JobNotifyMessage(jobIds));
×
366
                }
367
        }
×
368

369
        /**
370
         * Send a notification about a collection of machines changing.
371
         *
372
         * @param machineNames
373
         *            The machines that have changed. (Usually <em>all</em>
374
         *            machines.)
375
         * @throws IOException
376
         *             If network access fails.
377
         */
378
        protected final void writeMachineNotification(
379
                        List<String> machineNames)
380
                        throws IOException {
381
                if (!machineNames.isEmpty() && mayWrite()) {
×
382
                        sendMessage(new MachineNotifyMessage(machineNames));
×
383
                }
384
        }
×
385

386
        /**
387
         * Read a message from the client and send a response.
388
         *
389
         * @return {@code true} if further messages should be processed,
390
         *         {@code false} if the connection should be closed.
391
         * @throws IOException
392
         *             If network access fails.
393
         * @throws InterruptedException
394
         *             If interrupted (happens on service shutdown).
395
         */
396
        public final boolean communicate()
397
                        throws IOException, InterruptedException {
398
                Command cmd;
399
                try {
400
                        var c = readMessage();
1✔
401
                        if (!c.isPresent()) {
1✔
402
                                log.debug("null message");
1✔
403
                                return false;
1✔
404
                        }
405
                        cmd = c.orElseThrow();
1✔
406
                } catch (SocketTimeoutException e) {
×
407
                        log.trace("timeout");
×
408
                        // Message was not read by time timeout expired
409
                        return !currentThread().isInterrupted();
×
410
                } catch (MismatchedInputException e) {
1✔
411
                        log.error("Error on message reception: {}", e.getMessage());
1✔
412
                        writeException(e);
1✔
413
                        return true;
1✔
414
                } catch (JsonMappingException | JsonParseException e) {
×
415
                        log.error("Error on message reception", e);
×
416
                        writeException(e);
×
417
                        return true;
×
418
                }
1✔
419

420
                Object r;
421
                try {
422
                        r = callOperation(cmd);
1✔
423
                } catch (Oops | TaskException | IllegalArgumentException e) {
1✔
424
                        // Expected exceptions; don't log
425
                        writeException(e);
1✔
426
                        return true;
1✔
427
                } catch (Exception e) {
×
428
                        log.warn("unexpected exception from {} operation",
×
429
                                        cmd.getCommand(), e);
×
430
                        writeException(e);
×
431
                        return true;
×
432
                }
1✔
433

434
                log.debug("responded with {}", r);
1✔
435
                writeResponse(r);
1✔
436
                return true;
1✔
437
        }
438

439
        /**
440
         * Decode the command to convert into a method to call.
441
         *
442
         * @param cmd
443
         *            The command.
444
         * @return The result of the command. Can be anything (<em>including</em>
445
         *         {@code null}) as long as it can be serialised.
446
         * @throws Exception
447
         *             If things go wrong
448
         */
449
        private Object callOperation(Command cmd) throws Exception {
450
                log.debug("calling operation '{}'", cmd.getCommand());
1✔
451
                var args = cmd.getArgs();
1✔
452
                var kwargs = cmd.getKwargs();
1✔
453
                return switch (cmd.getCommand()) {
1✔
454
                case "create_job" -> {
455
                        // This is three operations really, and an optional parameter.
456
                        byte[] serialCmd = getJsonMapper().writeValueAsBytes(cmd);
1✔
457
                        // Checkstyle bug: indentation confusion
458
                        // CHECKSTYLE:OFF
459
                        yield switch (args.size()) {
1✔
460
                        case 0 -> createJobNumBoards(1, kwargs, serialCmd).orElse(null);
1✔
461
                        case 1 -> createJobNumBoards(parseDec(args, 0), kwargs, serialCmd)
1✔
462
                                        .orElse(null);
1✔
463
                        case 2 -> createJobRectangle(parseDec(args, 0), parseDec(args, 1),
1✔
464
                                        kwargs, serialCmd).orElse(null);
1✔
465
                        case TRIAD_COORD_COUNT ->
466
                                createJobSpecificBoard(new TriadCoords(parseDec(args, 0),
1✔
467
                                                parseDec(args, 1), parseDec(args, 2)), kwargs,
1✔
468
                                                serialCmd).orElse(null);
1✔
469
                        default -> throw new Oops(
1✔
470
                                        "unsupported number of arguments: " + args.size());
1✔
471
                        };
472
                        // CHECKSTYLE:ON
473
                }
474
                case "destroy_job" -> {
475
                        destroyJob(parseDec(args, 0), (String) kwargs.get("reason"));
1✔
476
                        yield null;
1✔
477
                }
478
                case "get_board_at_position" ->
479
                        requireNonNull(getBoardAtPhysicalPosition(
1✔
480
                                        (String) kwargs.get("machine_name"), parseDec(kwargs, "x"),
1✔
481
                                        parseDec(kwargs, "y"), parseDec(kwargs, "z")));
1✔
482
                case "get_board_position" -> requireNonNull(getBoardAtLogicalPosition(
1✔
483
                                (String) kwargs.get("machine_name"), parseDec(kwargs, "x"),
1✔
484
                                parseDec(kwargs, "y"), parseDec(kwargs, "z")));
1✔
485
                case "get_job_machine_info" ->
486
                        requireNonNull(getJobMachineInfo(parseDec(args, 0)));
1✔
487
                case "get_job_state" -> requireNonNull(getJobState(parseDec(args, 0)));
1✔
488
                case "job_keepalive" -> {
489
                        jobKeepalive(parseDec(args, 0));
1✔
490
                        yield null;
1✔
491
                }
492
                case "list_jobs" -> requireNonNull(listJobs());
1✔
493
                case "list_machines" -> requireNonNull(listMachines());
1✔
494
                case "no_notify_job" -> {
495
                        notifyJob(optInt(args), false);
1✔
496
                        yield null;
1✔
497
                }
498
                case "no_notify_machine" -> {
499
                        notifyMachine(optStr(args), false);
1✔
500
                        yield null;
1✔
501
                }
502
                case "notify_job" -> {
503
                        notifyJob(optInt(args), true);
1✔
504
                        yield null;
1✔
505
                }
506
                case "notify_machine" -> {
507
                        notifyMachine(optStr(args), true);
1✔
508
                        yield null;
1✔
509
                }
510
                case "power_off_job_boards" -> {
511
                        powerJobBoards(parseDec(args, 0), OFF);
1✔
512
                        yield null;
1✔
513
                }
514
                case "power_on_job_boards" -> {
515
                        powerJobBoards(parseDec(args, 0), ON);
×
516
                        yield null;
×
517
                }
518
                case "version" -> requireNonNull(version());
1✔
519
                case "where_is" -> {
520
                        // This is four operations in a trench coat
521
                        if (kwargs.containsKey("job_id")) {
1✔
522
                                yield requireNonNull(whereIsJobChip(parseDec(kwargs, "job_id"),
1✔
523
                                                parseDec(kwargs, "chip_x"),
1✔
524
                                                parseDec(kwargs, "chip_y")));
1✔
525
                        } else if (!kwargs.containsKey("machine")) {
1✔
526
                                throw new Oops("missing parameter: machine");
×
527
                        }
528
                        var m = (String) kwargs.get("machine");
1✔
529
                        if (kwargs.containsKey("chip_x")) {
1✔
530
                                yield requireNonNull(
1✔
531
                                                whereIsMachineChip(m, parseDec(kwargs, "chip_x"),
1✔
532
                                                                parseDec(kwargs, "chip_y")));
1✔
533
                        } else if (kwargs.containsKey("x")) {
1✔
534
                                yield requireNonNull(
1✔
535
                                                whereIsMachineLogicalBoard(m, parseDec(kwargs, "x"),
1✔
536
                                                                parseDec(kwargs, "y"), parseDec(kwargs, "z")));
1✔
537
                        } else if (kwargs.containsKey("cabinet")) {
1✔
538
                                yield requireNonNull(whereIsMachinePhysicalBoard(m,
1✔
539
                                                parseDec(kwargs, "cabinet"), parseDec(kwargs, "frame"),
1✔
540
                                                parseDec(kwargs, "board")));
1✔
541
                        } else {
542
                                throw new Oops("missing parameter: chip_x, x, or cabinet");
×
543
                        }
544
                }
545
                case "report_problem" -> {
546
                        var ip = args.get(0).toString();
×
547
                        Integer x = null, y = null, p = null;
×
548
                        var desc = "It doesn't work and I don't know why.";
×
549
                        if (kwargs.containsKey("x")) {
×
550
                                x = parseDec(kwargs, "x");
×
551
                                y = parseDec(kwargs, "y");
×
552
                                if (kwargs.containsKey("p")) {
×
553
                                        p = parseDec(kwargs, "p");
×
554
                                }
555
                        }
556
                        if (kwargs.containsKey("description")) {
×
557
                                desc = Objects.toString(kwargs.get("description"));
×
558
                        }
559
                        reportProblem(ip, x, y, p, desc);
×
560
                        yield null;
×
561
                }
562
                case "login" -> throw new Oops("upgrading security is not supported");
×
563
                default -> throw new Oops("unknown command: " + cmd.getCommand());
×
564
                };
565
        }
566

567
        /**
568
         * Create a job asking for a number of boards.
569
         *
570
         * @param numBoards
571
         *            Number of boards.
572
         * @param kwargs
573
         *            Keyword argument map.
574
         * @param cmd
575
         *            The actual command, as serialised JSON.
576
         * @return Job identifier.
577
         * @throws TaskException
578
         *             If anything goes wrong.
579
         */
580
        protected abstract Optional<Integer> createJobNumBoards(
581
                        @Positive int numBoards,
582
                        Map<@NotBlank String, @NotNull Object> kwargs, byte[] cmd)
583
                        throws TaskException;
584

585
        /**
586
         * Create a job asking for a rectangle of boards.
587
         *
588
         * @param width
589
         *            Width of rectangle in triads.
590
         * @param height
591
         *            Height of rectangle in triads.
592
         * @param kwargs
593
         *            Keyword argument map.
594
         * @param cmd
595
         *            The actual command, as serialised JSON.
596
         * @return Job identifier.
597
         * @throws TaskException
598
         *             If anything goes wrong.
599
         */
600
        protected abstract Optional<Integer> createJobRectangle(
601
                        @ValidTriadWidth int width, @ValidTriadHeight int height,
602
                        Map<@NotBlank String, @NotNull Object> kwargs, byte[] cmd)
603
                        throws TaskException;
604

605
        /**
606
         * Create a job asking for a specific board.
607
         *
608
         * @param coords
609
         *            Which board, by its logical coordinates.
610
         * @param kwargs
611
         *            Keyword argument map.
612
         * @param cmd
613
         *            The actual command, as serialised JSON.
614
         * @return Job identifier. Never {@code null}.
615
         * @throws TaskException
616
         *             If anything goes wrong.
617
         */
618
        protected abstract Optional<Integer> createJobSpecificBoard(
619
                        @Valid TriadCoords coords,
620
                        Map<@NotBlank String, @NotNull Object> kwargs, byte[] cmd)
621
                        throws TaskException;
622

623
        /**
624
         * Destroy a job.
625
         *
626
         * @param jobId
627
         *            Job identifier.
628
         * @param reason
629
         *            Why the machine is being destroyed.
630
         * @throws TaskException
631
         *             If anything goes wrong.
632
         */
633
        protected abstract void destroyJob(int jobId, String reason)
634
                        throws TaskException;
635

636
        /**
637
         * Get the coordinates of a board at a physical location.
638
         *
639
         * @param machineName
640
         *            Name of the machine.
641
         * @param cabinet
642
         *            Cabinet number.
643
         * @param frame
644
         *            Frame number.
645
         * @param board
646
         *            Board number.
647
         * @return Logical location. Never {@code null}.
648
         * @throws TaskException
649
         *             If anything goes wrong.
650
         */
651
        protected abstract BoardCoordinates getBoardAtPhysicalPosition(
652
                        @NotBlank String machineName, @ValidCabinetNumber int cabinet,
653
                        @ValidFrameNumber int frame, @ValidBoardNumber int board)
654
                        throws TaskException;
655

656
        /**
657
         * Get the physical location of a board at given coordinates.
658
         *
659
         * @param machineName
660
         *            Name of the machine.
661
         * @param x
662
         *            Triad X coordinate.
663
         * @param y
664
         *            Triad Y coordinate.
665
         * @param z
666
         *            Triad Z coordinate.
667
         * @return Physical location. Never {@code null}.
668
         * @throws TaskException
669
         *             If anything goes wrong.
670
         */
671
        protected abstract BoardPhysicalCoordinates getBoardAtLogicalPosition(
672
                        @NotBlank String machineName, @ValidTriadX int x,
673
                        @ValidTriadY int y, @ValidTriadZ int z) throws TaskException;
674

675
        /**
676
         * Get information about the machine allocated to a job.
677
         *
678
         * @param jobId
679
         *            Job identifier.
680
         * @return Description of job's (sub)machine. Never {@code null}.
681
         * @throws TaskException
682
         *             If anything goes wrong.
683
         */
684
        protected abstract JobMachineInfo getJobMachineInfo(int jobId)
685
                        throws TaskException;
686

687
        /**
688
         * Get the state of a job.
689
         *
690
         * @param jobId
691
         *            Job identifier.
692
         * @return State description. Never {@code null}.
693
         * @throws TaskException
694
         *             If anything goes wrong.
695
         */
696
        protected abstract JobState getJobState(int jobId) throws TaskException;
697

698
        /**
699
         * Mark a job as still being kept alive.
700
         *
701
         * @param jobId
702
         *            Job identifier.
703
         * @throws TaskException
704
         *             If anything goes wrong.
705
         */
706
        protected abstract void jobKeepalive(int jobId) throws TaskException;
707

708
        /**
709
         * List the jobs.
710
         *
711
         * @return Descriptions of jobs on all machines. Never {@code null}.
712
         * @throws TaskException
713
         *             If anything goes wrong.
714
         */
715
        protected abstract JobDescription[] listJobs() throws TaskException;
716

717
        /**
718
         * List the machines.
719
         *
720
         * @return Descriptions of all machines. Never {@code null}.
721
         * @throws TaskException
722
         *             If anything goes wrong.
723
         */
724
        protected abstract Machine[] listMachines() throws TaskException;
725

726
        /**
727
         * Request notification of job status changes. Best effort only.
728
         *
729
         * @param jobId
730
         *            Job identifier. May be {@code null} to talk about any job.
731
         * @param wantNotify
732
         *            Whether to enable or disable these notifications.
733
         * @throws TaskException
734
         *             If anything goes wrong.
735
         */
736
        protected abstract void notifyJob(Integer jobId, boolean wantNotify)
737
                        throws TaskException;
738

739
        /**
740
         * Request notification of machine status changes. Best effort only.
741
         *
742
         * @param machineName
743
         *            Name of the machine. May be {@code null} to talk about any
744
         *            machine.
745
         * @param wantNotify
746
         *            Whether to enable or disable these notifications.
747
         * @throws TaskException
748
         *             If anything goes wrong.
749
         */
750
        protected abstract void notifyMachine(String machineName,
751
                        boolean wantNotify) throws TaskException;
752

753
        /**
754
         * Switch on or off a job's boards.
755
         *
756
         * @param jobId
757
         *            Job identifier.
758
         * @param switchOn
759
         *            Whether to switch on.
760
         * @throws TaskException
761
         *             If anything goes wrong.
762
         */
763
        protected abstract void powerJobBoards(int jobId, PowerState switchOn)
764
                        throws TaskException;
765

766
        /**
767
         * Report a problem with a board, chip or core. If a whole chip has a
768
         * problem, {@code p} will be {@code null}. If a whole board has a problem,
769
         * {@code x,y} will be {@code null,null}.
770
         *
771
         * @param address
772
         *            The board's IP address.
773
         * @param x
774
         *            The chip's X coordinate.
775
         * @param y
776
         *            The chip's Y coordinate.
777
         * @param p
778
         *            The core's P coordinate.
779
         * @param description
780
         *            Optional descriptive text about the problem.
781
         */
782
        protected abstract void reportProblem(@IPAddress String address,
783
                        @ValidX Integer x, @ValidY Integer y, @ValidP Integer p,
784
                        String description);
785

786
        /**
787
         * Get the service version.
788
         *
789
         * @return The service version. Never {@code null}.
790
         * @throws TaskException
791
         *             If anything goes wrong.
792
         */
793
        protected abstract String version() throws TaskException;
794

795
        /**
796
         * Describe where a chip is within a job.
797
         *
798
         * @param jobId
799
         *            Job identifier.
800
         * @param x
801
         *            Chip X coordinate.
802
         * @param y
803
         *            Chip Y coordinate.
804
         * @return Descriptor. Never {@code null}.
805
         * @throws TaskException
806
         *             If anything goes wrong.
807
         */
808
        protected abstract WhereIs whereIsJobChip(int jobId, @ValidX int x,
809
                        @ValidY int y) throws TaskException;
810

811
        /**
812
         * Describe where a chip is within a machine.
813
         *
814
         * @param machineName
815
         *            Name of the machine.
816
         * @param x
817
         *            Chip X coordinate.
818
         * @param y
819
         *            Chip Y coordinate.
820
         * @return Descriptor. Never {@code null}.
821
         * @throws TaskException
822
         *             If anything goes wrong.
823
         */
824
        protected abstract WhereIs whereIsMachineChip(@NotBlank String machineName,
825
                        @ValidX int x, @ValidY int y) throws TaskException;
826

827
        /**
828
         * Describe where a board is within a machine.
829
         *
830
         * @param machineName
831
         *            Name of the machine.
832
         * @param x
833
         *            Triad X coordinate.
834
         * @param y
835
         *            Triad Y coordinate.
836
         * @param z
837
         *            Triad Z coordinate.
838
         * @return Descriptor. Never {@code null}.
839
         * @throws TaskException
840
         *             If anything goes wrong.
841
         */
842
        protected abstract WhereIs whereIsMachineLogicalBoard(
843
                        @NotBlank String machineName, @ValidTriadX int x,
844
                        @ValidTriadY int y, @ValidTriadZ int z) throws TaskException;
845

846
        /**
847
         * Describe where a board is within a machine.
848
         *
849
         * @param machineName
850
         *            Name of the machine.
851
         * @param cabinet
852
         *            Cabinet number.
853
         * @param frame
854
         *            Frame number.
855
         * @param board
856
         *            Board number.
857
         * @return Descriptor. Never {@code null}.
858
         * @throws TaskException
859
         *             If anything goes wrong.
860
         */
861
        protected abstract WhereIs whereIsMachinePhysicalBoard(String machineName,
862
                        @ValidCabinetNumber int cabinet, @ValidFrameNumber int frame,
863
                        @ValidBoardNumber int board) throws TaskException;
864

865
        private static Integer optInt(List<?> args) {
866
                return args.isEmpty() ? null : parseDec(args, 0);
1✔
867
        }
868

869
        private static String optStr(List<?> args) {
870
                return args.isEmpty() ? null : Objects.toString(args.get(0), null);
1✔
871
        }
872
}
873

874
/** Indicates a failure to parse a command. */
875
final class Oops extends RuntimeException {
876
        private static final long serialVersionUID = 1L;
877

878
        Oops(String msg) {
879
                super(msg);
1✔
880
        }
1✔
881
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc