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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

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

Pull #658

github

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

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

8373 of 22997 relevant lines covered (36.41%)

0.36 hits per line

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

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 javax.annotation.PostConstruct;
43
import javax.annotation.PreDestroy;
44
import javax.validation.Valid;
45
import javax.validation.constraints.NotBlank;
46
import javax.validation.constraints.NotNull;
47

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

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

58
import uk.ac.manchester.spinnaker.alloc.SpallocProperties;
59
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.StateControlProperties;
60
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs;
61
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs.Epoch;
62
import uk.ac.manchester.spinnaker.alloc.bmp.BMPController;
63
import uk.ac.manchester.spinnaker.alloc.bmp.BlacklistStore;
64
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
65
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.RowMapper;
66
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
67
import uk.ac.manchester.spinnaker.alloc.db.Row;
68
import uk.ac.manchester.spinnaker.alloc.model.BoardAndBMP;
69
import uk.ac.manchester.spinnaker.alloc.model.BoardIssueReport;
70
import uk.ac.manchester.spinnaker.alloc.model.BoardRecord;
71
import uk.ac.manchester.spinnaker.alloc.model.MachineTagging;
72
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
73
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
74
import uk.ac.manchester.spinnaker.messages.model.ADCInfo;
75
import uk.ac.manchester.spinnaker.messages.model.Blacklist;
76
import uk.ac.manchester.spinnaker.utils.validation.IPAddress;
77

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

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

89
        @Autowired
90
        private Epochs epochs;
91

92
        @Autowired
93
        private BlacklistStore blacklistStore;
94

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

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

103
        @Autowired
104
        private BMPController bmpController;
105

106
        private StateControlProperties props;
107

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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(MachineNameId::name, m -> getMachineReports
×
409
                                                                .call(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
                @Serial
469
                private static final long serialVersionUID = -6450838951059318431L;
470

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

822
                private final Epoch epoch;
823

824
                private final int boardId;
825

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

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

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

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

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

© 2025 Coveralls, Inc