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

SpiNNakerManchester / JavaSpiNNaker / 6185194352

14 Sep 2023 11:57AM UTC coverage: 36.982% (-0.004%) from 36.986%
6185194352

push

github

web-flow
Merge pull request #1057 from SpiNNakerManchester/skip-down-ebrains-hosts

Skip down services when building truststore

8683 of 23479 relevant lines covered (36.98%)

1.11 hits per line

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

31.54
/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.time.Instant;
32
import java.util.HashSet;
33
import java.util.List;
34
import java.util.Map;
35
import java.util.Optional;
36
import java.util.Set;
37
import java.util.concurrent.CancellationException;
38
import java.util.concurrent.ScheduledFuture;
39
import java.util.function.Function;
40

41
import javax.annotation.PostConstruct;
42
import javax.annotation.PreDestroy;
43
import javax.validation.Valid;
44
import javax.validation.constraints.NotBlank;
45
import javax.validation.constraints.NotNull;
46

47
import org.slf4j.Logger;
48
import org.springframework.beans.factory.annotation.Autowired;
49
import org.springframework.dao.DataAccessException;
50
import org.springframework.lang.NonNull;
51
import org.springframework.scheduling.TaskScheduler;
52
import org.springframework.stereotype.Service;
53

54
import com.google.errorprone.annotations.CompileTimeConstant;
55
import com.google.errorprone.annotations.MustBeClosed;
56

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 {
3✔
84
        private static final int SHORT_WAIT = 500;
85

86
        private static final Logger log = getLogger(MachineStateControl.class);
3✔
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();
3✔
113
                // After a minute, start retrieving board serial numbers
114
                readAllTask =
3✔
115
                                executor.schedule((Runnable) this::readAllBoardSerialNumbers,
3✔
116
                                                Instant.now().plus(props.getBlacklistTimeout()));
3✔
117
                // Why can't I pass a Duration directly there?
118
        }
3✔
119

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

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

144
                /** The machine ID. Unique. */
145
                public final int machineId;
146

147
                /** The board ID. Unique. */
148
                public final int id;
149

150
                /** The X triad coordinate. */
151
                public final int x;
152

153
                /** The Y triad coordinate. */
154
                public final int y;
155

156
                /** The Z triad coordinate. */
157
                public final int z;
158

159
                /** The cabinet number. */
160
                public final int cabinet;
161

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

165
                /** The board number. */
166
                public final Integer board;
167

168
                /** The IP address managed by the board's root chip. */
169
                public final String address;
170

171
                /** The BMP serial number, if known. */
172
                public final String bmpSerial;
173

174
                /** The physical board serial number, if known. */
175
                public final String physicalSerial;
176

177
                /** The BMP id. */
178
                public final int bmpId;
179

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

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

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

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

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

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

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

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

267
                @Override
268
                public String toString() {
269
                        return format("(%d,%d,%d)", x, y, z);
×
270
                }
271

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

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

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

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

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

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

388
        private class MachineNameId {
389
                int id;
390

391
                String name;
392

393
                MachineNameId(Row row) {
×
394
                        id = row.getInt("machine_id");
×
395
                        name = row.getString("machine_name");
×
396
                }
×
397
        }
398

399
        /**
400
         * @return The unacknowledged reports about boards with potential problems
401
         *         in existing machines, categorised by machine.
402
         */
403
        public Map<String, List<BoardIssueReport>> getMachineReports() {
404
                return executeRead(conn -> {
×
405
                        try (var getMachines = conn.query(GET_ALL_MACHINES);
×
406
                                        var getMachineReports = conn.query(GET_MACHINE_REPORTS)) {
×
407
                                return Row.stream(getMachines.call(MachineNameId::new, true))
×
408
                                                .toMap(m -> m.name, m -> getMachineReports.call(
×
409
                                                                BoardIssueReport::new, m.id));
×
410
                        }
411
                });
412
        }
413

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

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

460
        /**
461
         * Exception thrown when the machine state can't be read from or written to
462
         * a BMP. This includes when a blacklist can't be accessed or when the
463
         * machine state structure can't be read.
464
         *
465
         * @author Donal Fellows
466
         */
467
        public static final class MachineStateException extends RuntimeException {
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 board
484
         *            The board to get the blacklist of.
485
         * @return The blacklist that was transferred, if any.
486
         */
487
        public Optional<Blacklist> pullBlacklist(BoardState board) {
488
                try {
489
                        return readBlacklistFromMachine(board).map(bl -> {
×
490
                                blacklistStore.writeBlacklist(board.id, bl);
×
491
                                execute(c -> {
×
492
                                        // These must be done in ONE transaction
493
                                        changed(c, board.id);
×
494
                                        synched(c, board.id);
×
495
                                        return this; // Unimportant result
×
496
                                });
497
                                return bl;
×
498
                        });
499
                } catch (InterruptedException e) {
×
500
                        return Optional.empty();
×
501
                }
502
        }
503

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

808
        /**
809
         * Manages the transactions used to safely talk with the BMP controller.
810
         *
811
         * @author Donal Fellows
812
         */
813
        private final class Op implements AutoCloseable {
814
                private final int op;
815

816
                private final Epoch epoch;
817

818
                private final int boardId;
819

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

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

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

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

929
                /**
930
                 * Deletes the temporary operation description row in the DB.
931
                 * <p>
932
                 * {@inheritDoc}
933
                 */
934
                @Override
935
                public void close() {
936
                        execute(conn -> {
3✔
937
                                try (var delReq = conn.update(DELETE_BLACKLIST_OP)) {
3✔
938
                                        return delReq.call(op);
3✔
939
                                }
940
                        });
941
                }
3✔
942
        }
943
}
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