• 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

25.38
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/admin/MachineStateControl.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.admin;
17

18
import static java.lang.String.format;
19
import static java.lang.Thread.currentThread;
20
import static java.time.Instant.now;
21
import static java.util.Objects.requireNonNull;
22
import static org.slf4j.LoggerFactory.getLogger;
23
import static uk.ac.manchester.spinnaker.alloc.db.Row.bool;
24
import static uk.ac.manchester.spinnaker.alloc.db.Row.instant;
25
import static uk.ac.manchester.spinnaker.alloc.db.Row.integer;
26
import static uk.ac.manchester.spinnaker.alloc.db.Row.serial;
27
import static uk.ac.manchester.spinnaker.alloc.db.Row.string;
28
import static uk.ac.manchester.spinnaker.utils.CollectionUtils.batch;
29
import static uk.ac.manchester.spinnaker.utils.CollectionUtils.lmap;
30

31
import java.io.Serial;
32
import java.time.Instant;
33
import java.util.HashSet;
34
import java.util.List;
35
import java.util.Map;
36
import java.util.Optional;
37
import java.util.Set;
38
import java.util.concurrent.CancellationException;
39
import java.util.concurrent.ScheduledFuture;
40
import java.util.function.Function;
41

42
import org.slf4j.Logger;
43
import org.springframework.beans.factory.annotation.Autowired;
44
import org.springframework.dao.DataAccessException;
45
import org.springframework.lang.NonNull;
46
import org.springframework.scheduling.TaskScheduler;
47
import org.springframework.stereotype.Service;
48

49
import com.google.errorprone.annotations.CompileTimeConstant;
50
import com.google.errorprone.annotations.MustBeClosed;
51

52
import jakarta.annotation.PostConstruct;
53
import jakarta.annotation.PreDestroy;
54
import jakarta.validation.Valid;
55
import jakarta.validation.constraints.NotBlank;
56
import jakarta.validation.constraints.NotNull;
57
import uk.ac.manchester.spinnaker.alloc.SpallocProperties;
58
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.StateControlProperties;
59
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs;
60
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs.Epoch;
61
import uk.ac.manchester.spinnaker.alloc.bmp.BMPController;
62
import uk.ac.manchester.spinnaker.alloc.bmp.BlacklistStore;
63
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
64
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.RowMapper;
65
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
66
import uk.ac.manchester.spinnaker.alloc.db.Row;
67
import uk.ac.manchester.spinnaker.alloc.model.BoardAndBMP;
68
import uk.ac.manchester.spinnaker.alloc.model.BoardIssueReport;
69
import uk.ac.manchester.spinnaker.alloc.model.BoardRecord;
70
import uk.ac.manchester.spinnaker.alloc.model.MachineTagging;
71
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
72
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
73
import uk.ac.manchester.spinnaker.messages.model.ADCInfo;
74
import uk.ac.manchester.spinnaker.messages.model.Blacklist;
75
import uk.ac.manchester.spinnaker.utils.validation.IPAddress;
76

77
/**
78
 * How to manage the state of a machine and boards in it.
79
 *
80
 * @author Donal Fellows
81
 */
82
@Service
83
public class MachineStateControl extends DatabaseAwareBean {
1✔
84
        private static final int SHORT_WAIT = 500;
85

86
        private static final Logger log = getLogger(MachineStateControl.class);
1✔
87

88
        @Autowired
89
        private Epochs epochs;
90

91
        @Autowired
92
        private BlacklistStore blacklistStore;
93

94
        /** Just for {@link #launchBackground()}. */
95
        @Autowired
96
        private TaskScheduler executor;
97

98
        /** Just for {@link #launchBackground()}. */
99
        @Autowired
100
        private SpallocProperties properties;
101

102
        @Autowired
103
        private BMPController bmpController;
104

105
        private StateControlProperties props;
106

107
        /** Calls {@link #readAllBoardSerialNumbers()} after a delay. */
108
        private ScheduledFuture<?> readAllTask;
109

110
        @PostConstruct
111
        private void launchBackground() {
112
                props = properties.getStateControl();
1✔
113
                // After a minute, start retrieving board serial numbers
114
                // Don't do this if testing
115
                if (!properties.getTransceiver().isDummy()) {
1✔
116
                        readAllTask = executor.schedule(
×
117
                                        (Runnable) this::readAllBoardSerialNumbers,
118
                                        Instant.now().plus(props.getBlacklistTimeout()));
×
119
                        // Why can't I pass a Duration directly there?
120
                }
121
        }
1✔
122

123
        @PreDestroy
124
        private void stopBackground() {
125
                var t = readAllTask;
×
126
                if (t != null) {
×
127
                        readAllTask = null;
×
128
                        try {
129
                                t.cancel(true);
×
130
                                t.get();
×
131
                        } catch (InterruptedException | CancellationException e) {
×
132
                                log.trace("stopped background loader", e);
×
133
                        } catch (Exception e) {
×
134
                                log.info("failure in background board serial number fetch", e);
×
135
                        }
×
136
                }
137
        }
×
138

139
        /**
140
         * Access to the enablement-state of a board.
141
         */
142
        public final class BoardState {
143
                // No validity definitions; instances originate in DB
144
                /** The name of the containing SpiNNaker machine. */
145
                public final String machineName;
146

147
                /** The machine ID. Unique. */
148
                public final int machineId;
149

150
                /** The board ID. Unique. */
151
                public final int id;
152

153
                /** The X triad coordinate. */
154
                public final int x;
155

156
                /** The Y triad coordinate. */
157
                public final int y;
158

159
                /** The Z triad coordinate. */
160
                public final int z;
161

162
                /** The cabinet number. */
163
                public final int cabinet;
164

165
                /** The frame number. */
166
                public final int frame;
167

168
                /** The board number. */
169
                public final Integer board;
170

171
                /** The IP address managed by the board's root chip. */
172
                public final String address;
173

174
                /** The BMP serial number, if known. */
175
                public final String bmpSerial;
176

177
                /** The physical board serial number, if known. */
178
                public final String physicalSerial;
179

180
                /** The BMP id. */
181
                public final int bmpId;
182

183
                private BoardState(Row row) {
1✔
184
                        this.id = row.getInt("board_id");
1✔
185
                        this.x = row.getInt("x");
1✔
186
                        this.y = row.getInt("y");
1✔
187
                        this.z = row.getInt("z");
1✔
188
                        this.cabinet = row.getInt("cabinet");
1✔
189
                        this.frame = row.getInt("frame");
1✔
190
                        this.board = row.getInteger("board_num");
1✔
191
                        this.address = row.getString("address");
1✔
192
                        this.machineName = row.getString("machine_name");
1✔
193
                        this.bmpSerial = row.getString("bmp_serial_id");
1✔
194
                        this.physicalSerial = row.getString("physical_serial_id");
1✔
195
                        this.bmpId = row.getInt("bmp_id");
1✔
196
                        this.machineId = row.getInt("machine_id");
1✔
197
                }
1✔
198

199
                /**
200
                 * @return The allocatable state of the board. If a board is not
201
                 *         allocatable, it will not be handed out in new allocations to
202
                 *         jobs, but can continue to be used by whatever job it is
203
                 *         currently allocated to (if any).
204
                 */
205
                public boolean getState() {
206
                        return executeRead(conn -> {
×
207
                                try (var q = conn.query(GET_FUNCTIONING_FIELD)) {
×
208
                                        return q.call1(bool("functioning"), id).orElse(false);
×
209
                                }
210
                        });
211
                }
212

213
                /**
214
                 * @param newValue
215
                 *            The allocatable state to set the board to.
216
                 */
217
                public void setState(boolean newValue) {
218
                        execute(conn -> {
×
219
                                try (var u = conn.update(SET_FUNCTIONING_FIELD)) {
×
220
                                        return u.call(newValue, id);
×
221
                                }
222
                        });
223
                }
×
224

225
                /** @return What job has been allocated to the board? */
226
                public Optional<Integer> getAllocatedJob() {
227
                        return executeRead(conn -> {
×
228
                                try (var q = conn.query(GET_BOARD_JOB)) {
×
229
                                        return q.call1(integer("allocated_job"), id);
×
230
                                }
231
                        });
232
                }
233

234
                /** @return Is the board switched on? */
235
                public boolean getPower() {
236
                        return executeRead(conn -> {
×
237
                                try (var q = conn.query(GET_BOARD_POWER_INFO)) {
×
238
                                        return q.call1(bool("board_power"), id).orElse(false);
×
239
                                }
240
                        });
241
                }
242

243
                /** @return When was the board last switched on? */
244
                public Optional<Instant> getPowerOnTime() {
245
                        return executeRead(conn -> {
×
246
                                try (var q = conn.query(GET_BOARD_POWER_INFO)) {
×
247
                                        return q.call1(instant("power_on_timestamp"), id);
×
248
                                }
249
                        });
250
                }
251

252
                /** @return When was the board last switched off? */
253
                public Optional<Instant> getPowerOffTime() {
254
                        return executeRead(conn -> {
×
255
                                try (var q = conn.query(GET_BOARD_POWER_INFO)) {
×
256
                                        return q.call1(instant("power_off_timestamp"), id);
×
257
                                }
258
                        });
259
                }
260

261
                /** @return What issues have been logged against the board? */
262
                public List<BoardIssueReport> getReports() {
263
                        return executeRead(conn -> {
×
264
                                try (var q = conn.query(GET_BOARD_REPORTS)) {
×
265
                                        return q.call(BoardIssueReport::new, id);
×
266
                                }
267
                        });
268
                }
269

270
                @Override
271
                public String toString() {
272
                        return format("(%d,%d,%d)", x, y, z);
×
273
                }
274

275
                /**
276
                 * Convert this active object to a static record.
277
                 *
278
                 * @return The static (conceptually serialisable) record object.
279
                 */
280
                public BoardRecord toBoardRecord() {
281
                        var br = new BoardRecord();
×
282
                        br.setId(id);
×
283
                        br.setBmpId(bmpId);
×
284
                        br.setMachineName(machineName);
×
285
                        br.setX(x);
×
286
                        br.setY(y);
×
287
                        br.setZ(z);
×
288
                        br.setCabinet(cabinet);
×
289
                        br.setFrame(frame);
×
290
                        br.setBoard(board);
×
291
                        br.setIpAddress(address);
×
292
                        br.setBmpSerial(bmpSerial);
×
293
                        br.setPhysicalSerial(physicalSerial);
×
294
                        br.setLastPowerOn(getPowerOnTime().orElse(null));
×
295
                        br.setLastPowerOff(getPowerOffTime().orElse(null));
×
296
                        br.setPowered(getPower());
×
297
                        br.setJobId(getAllocatedJob().orElse(null));
×
298
                        br.setReports(getReports());
×
299
                        br.setEnabled(getState());
×
300
                        return br;
×
301
                }
302
        }
303

304
        /**
305
         * Look up a board for management.
306
         *
307
         * @param id
308
         *            The unique ID of the board. Because this is fully unique, the
309
         *            machine name is not needed.
310
         * @return Board state manager
311
         */
312
        public Optional<BoardState> findId(int id) {
313
                return executeRead(conn -> {
1✔
314
                        try (var q = conn.query(FIND_BOARD_BY_ID)) {
1✔
315
                                return q.call1(BoardState::new, id);
1✔
316
                        }
317
                });
318
        }
319

320
        /**
321
         * Look up a board for management.
322
         *
323
         * @param machine
324
         *            The name of the machine.
325
         * @param coords
326
         *            Triad coordinates
327
         * @return Board state manager
328
         */
329
        public Optional<BoardState> findTriad(@NotBlank String machine,
330
                        @Valid @NonNull TriadCoords coords) {
331
                return executeRead(conn -> {
×
332
                        try (var q = conn.query(FIND_BOARD_BY_NAME_AND_XYZ)) {
×
333
                                return q.call1(BoardState::new, machine, coords.x(), coords.y(),
×
334
                                                coords.z());
×
335
                        }
336
                });
337
        }
338

339
        /**
340
         * Look up a board for management.
341
         *
342
         * @param machine
343
         *            The name of the machine.
344
         * @param coords
345
         *            Physical coordinates
346
         * @return Board state manager
347
         */
348
        public Optional<BoardState> findPhysical(@NotBlank String machine,
349
                        @Valid @NotNull PhysicalCoords coords) {
350
                return executeRead(conn -> {
×
351
                        try (var q = conn.query(FIND_BOARD_BY_NAME_AND_CFB)) {
×
352
                                return q.call1(BoardState::new, machine, coords.c(), coords.f(),
×
353
                                                coords.b());
×
354
                        }
355
                });
356
        }
357

358
        /**
359
         * Look up a board for management.
360
         *
361
         * @param machine
362
         *            The name of the machine.
363
         * @param address
364
         *            Board IP address
365
         * @return Board state manager
366
         */
367
        public Optional<BoardState> findIP(@NotBlank String machine,
368
                        @IPAddress String address) {
369
                return executeRead(conn -> {
×
370
                        try (var q = conn.query(FIND_BOARD_BY_NAME_AND_IP_ADDRESS)) {
×
371
                                return q.call1(BoardState::new, machine, address);
×
372
                        }
373
                });
374
        }
375

376
        /**
377
         * @return The mapping from machine names+IDs to tags.
378
         */
379
        public List<MachineTagging> getMachineTagging() {
380
                return executeRead(conn -> {
×
381
                        try (var getMachines = conn.query(GET_ALL_MACHINES);
×
382
                                        var getTags = conn.query(GET_TAGS)) {
×
383
                                var infos = getMachines.call(MachineTagging::new, true);
×
384
                                for (var t : infos) {
×
385
                                        t.setTags(getTags.call(string("tag"), t.getId()));
×
386
                                }
×
387
                                return infos;
×
388
                        }
389
                });
390
        }
391

392
        /**
393
         * @return The unacknowledged reports about boards with potential problems
394
         *         in existing machines, categorised by machine.
395
         */
396
        public Map<String, List<BoardIssueReport>> getMachineReports() {
397
                record MachineNameId(int id, String name) {
×
398
                        MachineNameId(Row row) {
399
                                this(row.getInt("machine_id"), row.getString("machine_name"));
×
400
                        }
×
401
                }
402

403
                return executeRead(conn -> {
×
404
                        try (var getMachines = conn.query(GET_ALL_MACHINES);
×
405
                                        var getMachineReports = conn.query(GET_MACHINE_REPORTS)) {
×
406
                                return Row.stream(getMachines.call(MachineNameId::new, true))
×
407
                                                .toMap(MachineNameId::name, m -> getMachineReports
×
408
                                                                .call(BoardIssueReport::new, m.id()));
×
409
                        }
410
                });
411
        }
412

413
        /**
414
         * Replace the tags on a machine with a given set.
415
         *
416
         * @param machineName
417
         *            The name of the machine to update the tags of.
418
         * @param tags
419
         *            The tags to apply. Existing tags will be removed.
420
         * @throws IllegalArgumentException
421
         *             If the machine with that name doesn't exist.
422
         */
423
        public void updateTags(@NotBlank String machineName,
424
                        Set<@NotBlank String> tags) {
425
                execute(conn -> {
×
426
                        try (var getMachine = conn.query(GET_NAMED_MACHINE);
×
427
                                        var deleteTags = conn.update(DELETE_MACHINE_TAGS);
×
428
                                        var addTag = conn.update(INSERT_TAG)) {
×
429
                                int machineId = getMachine.call1(integer("machine_id"),
×
430
                                                machineName, true).orElseThrow(
×
431
                                                () -> new IllegalArgumentException("no such machine"));
×
432
                                deleteTags.call(machineId);
×
433
                                for (var tag : tags) {
×
434
                                        addTag.call(machineId, tag);
×
435
                                }
×
436
                                return this; // Unimportant value
×
437
                        }
438
                });
439
        }
×
440

441
        /**
442
         * Sets whether a machine is in service.
443
         *
444
         * @param machineName
445
         *            The name of the machine to control
446
         * @param inService
447
         *            Whether to put the machine in or out of service.
448
         */
449
        public void setMachineState(@NotBlank String machineName,
450
                        boolean inService) {
451
                execute(conn -> {
×
452
                        try (var setState = conn.update(SET_MACHINE_STATE)) {
×
453
                                setState.call(inService, machineName);
×
454
                                return this; // Unimportant value
×
455
                        }
456
                });
457
        }
×
458

459
        /**
460
         * Exception thrown when the machine state can't be read from or written to
461
         * a BMP. This includes when a blacklist can't be accessed or when the
462
         * machine state structure can't be read.
463
         *
464
         * @author Donal Fellows
465
         */
466
        public static final class MachineStateException extends RuntimeException {
467
                @Serial
468
                private static final long serialVersionUID = -6450838951059318431L;
469

470
                private MachineStateException(String msg, Exception exn) {
471
                        super(msg, exn);
×
472
                }
×
473

474
                private MachineStateException(String msg) {
475
                        super(msg);
×
476
                }
×
477
        }
478

479
        /**
480
         * Retrieve the blacklist for the given board from the board and store it in
481
         * the database.
482
         *
483
         * @param boardId
484
         *            The board to get the blacklist of.
485
         * @param bmpId
486
         *            The BMP of the board.
487
         * @return The blacklist that was transferred, if any.
488
         */
489
        public Optional<Blacklist> pullBlacklist(int boardId, int bmpId) {
490
                try {
491
                        return readBlacklistFromMachine(boardId, bmpId).map(bl -> {
×
492
                                blacklistStore.writeBlacklist(boardId, bl);
×
493
                                execute(c -> {
×
494
                                        // These must be done in ONE transaction
495
                                        changed(c, boardId);
×
496
                                        synched(c, boardId);
×
497
                                        return this; // Unimportant result
×
498
                                });
499
                                return bl;
×
500
                        });
501
                } catch (InterruptedException e) {
×
502
                        return Optional.empty();
×
503
                }
504
        }
505

506
        /**
507
         * Take the blacklist for the given board in the database and write it to
508
         * the board.
509
         *
510
         * @param board
511
         *            The board to set the blacklist of.
512
         * @return The blacklist that was transferred, if any.
513
         */
514
        public Optional<Blacklist> pushBlacklist(BoardState board) {
515
                return readBlacklistFromDB(board).map(bl -> {
×
516
                        try {
517
                                writeBlacklistToMachine(board.id, board.bmpId, bl);
×
518
                                execute(c -> synched(c, board.id)); // Unimportant result
×
519
                                return bl;
×
520
                        } catch (InterruptedException e) {
×
521
                                return null;
×
522
                        }
523
                });
524
        }
525

526
        private boolean changed(Connection conn, int boardId) {
527
                try (var synched = conn.update(MARK_BOARD_BLACKLIST_CHANGED)) {
×
528
                        return synched.call(boardId) > 0;
×
529
                }
530
        }
531

532
        private boolean synched(Connection conn, int boardId) {
533
                try (var synched = conn.update(MARK_BOARD_BLACKLIST_SYNCHED)) {
×
534
                        return synched.call(boardId) > 0;
×
535
                }
536
        }
537

538
        /**
539
         * Ensure that the database has the actual serial numbers of all boards in a
540
         * machine.
541
         *
542
         * @param machineName
543
         *            Which machine to read the serial numbers of.
544
         */
545
        public void readAllBoardSerialNumbers(@NotBlank String machineName) {
546
                scheduleSerialNumberReads(requireNonNull(machineName));
×
547
        }
×
548

549
        /**
550
         * Ensure that the database has the actual serial numbers of all known
551
         * boards.
552
         */
553
        private void readAllBoardSerialNumbers() {
554
                scheduleSerialNumberReads(null);
×
555
        }
×
556

557
        /**
558
         * Common core of {@link #readAllBoardSerialNumbers(String)} and
559
         * {@link #readAllBoardSerialNumbers()}.
560
         *
561
         * @param machineName
562
         *            The machine name, or {@code null} for all.
563
         */
564
        @SuppressWarnings("MustBeClosed")
565
        private void scheduleSerialNumberReads(String machineName) {
566
                batchReqs(machineName, "retrieving serial numbers",
×
567
                                props.getSerialReadBatchSize(),
×
568
                                id -> new BmpOp(CREATE_SERIAL_READ_REQ, id), BmpOp::completed);
×
569
        }
×
570

571
        private interface InterruptableConsumer<T> {
572
                /**
573
                 * Performs this operation on the given argument.
574
                 *
575
                 * @param t
576
                 *            the input argument
577
                 * @throws InterruptedException
578
                 *             if interrupted
579
                 */
580
                void accept(T t) throws InterruptedException;
581
        }
582

583
        /**
584
         * Perform an action for all boards in a machine (or all known), batching
585
         * them as necessary. If interrupted, will complete the currently processing
586
         * batch but will not perform further batches.
587
         *
588
         * @param machineName
589
         *            Which machine are we talking about? If {@code null}, all
590
         *            boards of all machines will be processed.
591
         * @param action
592
         *            What are we doing (for log messages)?
593
         * @param batchSize
594
         *            How many requests to handle at once in a batch of requests to
595
         *            the back end engine.
596
         * @param opGenerator
597
         *            How to generate an individual operation to perform.
598
         * @param opResultsHandler
599
         *            How to process the results of an individual operation.
600
         */
601
        private void batchReqs(String machineName, String action, int batchSize,
602
                        Function<Integer, BmpOp> opGenerator,
603
                        InterruptableConsumer<BmpOp> opResultsHandler) {
604
                var boards = executeRead(c -> listAllBoards(c, machineName));
×
605
                for (var batch : batch(batchSize, boards)) {
×
606
                        /*
607
                         * Theoretically, this could be more efficiently done. Practically,
608
                         * a proper multi-op scheme is really complex, even before
609
                         * considering how to handle failure modes! This isn't a performance
610
                         * sensitive part of the code.
611
                         */
612
                        var ops = lmap(batch, board -> opGenerator.apply(board.boardId));
×
613
                        var bmps = new HashSet<>(lmap(batch, board -> board.bmp.bmpId));
×
614
                        boolean stop = false;
×
615
                        try {
616
                                bmpController.triggerSearch(bmps);
×
617
                                for (var op : ops) {
×
618
                                        try {
619
                                                opResultsHandler.accept(op);
×
620
                                        } catch (RuntimeException e) {
×
621
                                                log.warn("failed while {}", action, e);
×
622
                                        } catch (InterruptedException e) {
×
623
                                                log.info("interrupted while {}", action, e);
×
624
                                                stop = true;
×
625
                                        }
×
626
                                }
×
627
                        } finally {
628
                                ops.forEach(BmpOp::close);
×
629
                        }
630
                        if (stop) {
×
631
                                // Mark as interrupted
632
                                currentThread().interrupt();
×
633
                                break;
×
634
                        }
635
                }
×
636
        }
×
637

638
        /**
639
         * Retrieve all blacklists, parse them, and store them in the DB's model.
640
         *
641
         * @param machineName
642
         *            Which machine to get the blacklists of.
643
         */
644
        @SuppressWarnings("MustBeClosed")
645
        public void updateAllBlacklists(@NotBlank String machineName) {
646
                batchReqs(requireNonNull(machineName), "retrieving blacklists",
×
647
                                props.getBlacklistReadBatchSize(),
×
648
                                id -> new BmpOp(CREATE_BLACKLIST_READ, id),
×
649
                                op -> op.getResult(serial("data", Blacklist.class))
×
650
                                                .ifPresent(bl -> {
×
651
                                                        blacklistStore.writeBlacklist(op.boardId, bl);
×
652
                                                        execute(c -> {
×
653
                                                                // These must be done in ONE transaction
654
                                                                changed(c, op.boardId);
×
655
                                                                synched(c, op.boardId);
×
656
                                                                return this; // Unimportant result
×
657
                                                        });
658
                                                }));
×
659
        }
×
660

661
        private static List<BoardAndBMP> listAllBoards(Connection conn,
662
                        String machineName) {
663
                try (var machines = conn.query(GET_NAMED_MACHINE);
×
664
                                var boards = conn.query(GET_ALL_BOARDS);
×
665
                                var all = conn.query(GET_ALL_BOARDS_OF_ALL_MACHINES)) {
×
666
                        if (machineName == null) {
×
667
                                return all.call(BoardAndBMP::new);
×
668
                        }
669
                        return machines.call1(integer("machine_id"), machineName).map(
×
670
                                        mid -> boards.call(BoardAndBMP::new, mid))
×
671
                                        .orElse(List.of());
×
672
                }
×
673
        }
674

675
        /**
676
         * Given a board, read its blacklist from the database.
677
         *
678
         * @param board
679
         *            Which board to read the blacklist of.
680
         * @return The board's blacklist.
681
         * @throws DataAccessException
682
         *             If access to the DB fails.
683
         */
684
        public Optional<Blacklist> readBlacklistFromDB(BoardState board) {
685
                return blacklistStore.readBlacklist(board.id);
×
686
        }
687

688
        /**
689
         * Given a board, write a blacklist for it to the database. Does
690
         * <em>not</em> push the blacklist to the board.
691
         *
692
         * @param boardId The ID of the board to write.
693
         * @param blacklist
694
         *            The blacklist to write.
695
         * @throws DataAccessException
696
         *             If access to the DB fails.
697
         */
698
        public void writeBlacklistToDB(int boardId,
699
                        @Valid Blacklist blacklist) {
700
                blacklistStore.writeBlacklist(boardId, blacklist);
×
701
                execute(c -> changed(c, boardId)); // Unimportant result
×
702
        }
×
703

704
        /**
705
         * Given a board, read its blacklist off the machine.
706
         *
707
         * @param boardId
708
         *            Which board to read the blacklist of.
709
         * @param bmpId
710
         *            The BMP of the board.
711
         * @return The board's blacklist.
712
         * @throws DataAccessException
713
         *             If access to the DB fails.
714
         * @throws MachineStateException
715
         *             If the read fails.
716
         * @throws InterruptedException
717
         *             If interrupted.
718
         */
719
        public Optional<Blacklist> readBlacklistFromMachine(int boardId, int bmpId)
720
                        throws InterruptedException {
721
                try (var op = new BmpOp(CREATE_BLACKLIST_READ, boardId)) {
1✔
722
                        bmpController.triggerSearch(List.of(bmpId));
1✔
723
                        return op.getResult(serial("data", Blacklist.class));
1✔
724
                }
725
        }
726

727
        /**
728
         * Write a blacklist to a board on the machine.
729
         *
730
         * @param boardId
731
         *            Which board to write the blacklist of.
732
         * @param bmpId
733
         *            Which BMP the board belongs to.
734
         * @param blacklist
735
         *            The blacklist to write.
736
         * @throws DataAccessException
737
         *             If access to the DB fails.
738
         * @throws MachineStateException
739
         *             If the write fails.
740
         * @throws InterruptedException
741
         *             If interrupted. Note that interrupting the thread does
742
         *             <em>not</em> necessarily halt the write of the blacklist.
743
         */
744
        public void writeBlacklistToMachine(int boardId, int bmpId,
745
                        @Valid Blacklist blacklist) throws InterruptedException {
746
                try (var op = new BmpOp(CREATE_BLACKLIST_WRITE, boardId, blacklist)) {
1✔
747
                        bmpController.triggerSearch(List.of(bmpId));
1✔
748
                        op.completed();
1✔
749
                }
750
        }
1✔
751

752
        /**
753
         * Read the serial number off a board.
754
         *
755
         * @param board
756
         *            Which board to get the serial number of.
757
         * @return The serial number.
758
         * @throws DataAccessException
759
         *             If access to the DB fails.
760
         * @throws MachineStateException
761
         *             If the write fails.
762
         * @throws InterruptedException
763
         *             If interrupted.
764
         */
765
        public String getSerialNumber(BoardState board)
766
                        throws InterruptedException {
767
                try (var op = new BmpOp(CREATE_SERIAL_READ_REQ, board.id)) {
1✔
768
                        bmpController.triggerSearch(List.of(board.bmpId));
1✔
769
                        op.completed();
1✔
770
                }
771
                // Can now read out of the DB normally
772
                return findId(board.id).map(b -> b.bmpSerial).orElse(null);
1✔
773
        }
774

775
        /**
776
         * Given a board, read its temperature data off the machine.
777
         *
778
         * @param boardId
779
         *            Which board to read the temperature data of.
780
         * @return The board's temperature data.
781
         * @throws DataAccessException
782
         *             If access to the DB fails.
783
         * @throws MachineStateException
784
         *             If the read fails.
785
         * @throws InterruptedException
786
         *             If interrupted.
787
         */
788
        public Optional<ADCInfo> readTemperatureFromMachine(int boardId)
789
                        throws InterruptedException {
790
                try (var op = new BmpOp(CREATE_TEMP_READ_REQ, boardId)) {
×
791
                        return op.getResult(serial("data", ADCInfo.class));
×
792
                }
793
        }
794

795
        /**
796
         * Test whether a board's blacklist is believed to be synchronised to the
797
         * hardware.
798
         *
799
         * @param board
800
         *            Which board?
801
         * @return True if the synch has happened, i.e., the time the blacklist data
802
         *         was changed is no later than the last time the synch happened.
803
         */
804
        public boolean isBlacklistSynched(BoardState board) {
805
                return executeRead(conn -> {
×
806
                        try (var isCurrent = conn.query(IS_BOARD_BLACKLIST_CURRENT)) {
×
807
                                return isCurrent.call1(bool("current"), board.id)
×
808
                                                .orElse(false);
×
809
                        }
810
                });
811
        }
812

813
        /**
814
         * Manages the transactions used to safely talk with the BMP controller.
815
         *
816
         * @author Donal Fellows
817
         */
818
        private final class BmpOp implements AutoCloseable {
819
                private final int op;
820

821
                private final Epoch epoch;
822

823
                private final int boardId;
824

825
                /**
826
                 * @param operation
827
                 *            The SQL to create the operation to carry out. Must
828
                 *            generate an ID, so presumably is an {@code INSERT}.
829
                 * @param args
830
                 *            Values to bind to parameters in the SQL. The first
831
                 *            parameter <em>must</em> be the board ID! Presumably that
832
                 *            will be the board that the operation refers to.
833
                 */
834
                @MustBeClosed
835
                @SuppressWarnings("CompileTimeConstant")
836
                BmpOp(@CompileTimeConstant final String operation, Object... args) {
1✔
837
                        boardId = (Integer) args[0];
1✔
838
                        var e = epochs.getBlacklistEpoch(boardId);
1✔
839
                        op = execute(conn -> {
1✔
840
                                try (var readReq = conn.update(operation)) {
1✔
841
                                        return readReq.key(args);
1✔
842
                                }
843
                        }).orElseThrow(() -> new MachineStateException(
1✔
844
                                        "could not create machine state request"));
845
                        if (!e.isValid()) {
1✔
846
                                log.warn("early board epoch invalidation");
×
847
                                e = epochs.getBlacklistEpoch(boardId);
×
848
                        }
849
                        epoch = e;
1✔
850
                }
1✔
851

852
                /**
853
                 * Wait for the result of the request to be ready.
854
                 *
855
                 * @param <T>
856
                 *            The type of the result value.
857
                 * @param retriever
858
                 *            How to convert the row containing the result into the
859
                 *            actual result of the transaction.
860
                 * @return The wrapped result, or empty if the operation times out.
861
                 * @throws InterruptedException
862
                 *             If the thread is interrupted.
863
                 * @throws MachineStateException
864
                 *             If the BMP throws an exception.
865
                 * @throws DataAccessException
866
                 *             If there is a problem accessing the database.
867
                 */
868
                <T> Optional<T> getResult(RowMapper<T> retriever)
869
                                throws InterruptedException, MachineStateException,
870
                                DataAccessException {
871
                        var end = now().plus(props.getBlacklistTimeout());
1✔
872
                        boolean once = false;
1✔
873
                        while (end.isAfter(now())) {
1✔
874
                                var result = executeRead(conn -> {
1✔
875
                                        try (var getResult =
1✔
876
                                                        conn.query(GET_COMPLETED_BLACKLIST_OP)) {
1✔
877
                                                return getResult.call1(row -> retriever.mapRow(
1✔
878
                                                                throwIfFailed(row)), op);
1✔
879
                                        }
880
                                });
881
                                if (result.isPresent()) {
1✔
882
                                        return result;
1✔
883
                                }
884
                                if (!epoch.isValid()) {
1✔
885
                                        // Things are going wonky; fall back on regular polling
886
                                        if (!once) {
×
887
                                                log.warn("epoch invalid for board {}?", boardId);
×
888
                                                once = true;
×
889
                                        }
890
                                        Thread.sleep(SHORT_WAIT);
×
891
                                } else {
892
                                        log.debug("Waiting for blacklist change");
1✔
893
                                        epoch.waitForChange(props.getBlacklistPoll());
1✔
894
                                }
895
                        }
1✔
896
                        return Optional.empty();
×
897
                }
898

899
                /**
900
                 * Wait for the result of the request to be ready, then discard that
901
                 * result. Used instead of {@link #getResult(Function)} when the value
902
                 * of the result is not interesting at all.
903
                 *
904
                 * @throws InterruptedException
905
                 *             If the thread is interrupted.
906
                 * @throws MachineStateException
907
                 *             If the BMP throws an exception.
908
                 * @throws DataAccessException
909
                 *             If there is a problem accessing the database.
910
                 */
911
                void completed() throws DataAccessException, MachineStateException,
912
                                InterruptedException {
913
                        getResult(__ -> this);
1✔
914
                }
1✔
915

916
                /**
917
                 * If a row encodes a failure state, unpack the exception from the row
918
                 * and throw it as a wrapped exception. Otherwise, just pass on the row.
919
                 *
920
                 * @param row
921
                 *            The row to examine.
922
                 * @return The row, which is now guaranteed to not be a failure.
923
                 * @throws MachineStateException
924
                 *             If the row encodes a failure.
925
                 */
926
                private Row throwIfFailed(Row row) throws MachineStateException {
927
                        if (row.getBoolean("failed")) {
1✔
928
                                throw new MachineStateException(
×
929
                                                "failed to access hardware state",
930
                                                row.getSerial("failure", Exception.class));
×
931
                        }
932
                        return row;
1✔
933
                }
934

935
                /**
936
                 * Deletes the temporary operation description row in the DB.
937
                 * <p>
938
                 * {@inheritDoc}
939
                 */
940
                @Override
941
                public void close() {
942
                        execute(conn -> {
1✔
943
                                try (var delReq = conn.update(DELETE_BLACKLIST_OP)) {
1✔
944
                                        return delReq.call(op);
1✔
945
                                }
946
                        });
947
                }
1✔
948
        }
949
}
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