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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

19 Sep 2023 08:46AM UTC coverage: 36.409% (-0.6%) from 36.982%
6233274834

Pull #658

github

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

1656 of 1656 new or added lines in 260 files covered. (100.0%)

8373 of 22997 relevant lines covered (36.41%)

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 javax.validation.Valid;
47
import javax.validation.constraints.NotBlank;
48
import javax.validation.constraints.NotNull;
49
import javax.validation.constraints.Positive;
50

51
import org.slf4j.Logger;
52

53
import com.fasterxml.jackson.core.JsonParseException;
54
import com.fasterxml.jackson.databind.JsonMappingException;
55
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
56
import com.google.errorprone.annotations.concurrent.GuardedBy;
57

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

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

88
        private static final int TRIAD_COORD_COUNT = 3;
89

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

879
        Oops(String msg) {
880
                super(msg);
1✔
881
        }
1✔
882
}
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