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

SpiNNakerManchester / JavaSpiNNaker / 11123659092

01 Oct 2024 10:30AM UTC coverage: 37.06% (-0.03%) from 37.094%
11123659092

push

github

web-flow
Merge pull request #1177 from SpiNNakerManchester/dependabot/maven/org.json-json-20240303

Bump org.json:json from 20231013 to 20240303

8767 of 23656 relevant lines covered (37.06%)

1.11 hits per line

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

25.95
/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.setBmpId(bmpId);
×
281
                        br.setMachineName(machineName);
×
282
                        br.setX(x);
×
283
                        br.setY(y);
×
284
                        br.setZ(z);
×
285
                        br.setCabinet(cabinet);
×
286
                        br.setFrame(frame);
×
287
                        br.setBoard(board);
×
288
                        br.setIpAddress(address);
×
289
                        br.setBmpSerial(bmpSerial);
×
290
                        br.setPhysicalSerial(physicalSerial);
×
291
                        br.setLastPowerOn(getPowerOnTime().orElse(null));
×
292
                        br.setLastPowerOff(getPowerOffTime().orElse(null));
×
293
                        br.setPowered(getPower());
×
294
                        br.setJobId(getAllocatedJob().orElse(null));
×
295
                        br.setReports(getReports());
×
296
                        br.setEnabled(getState());
×
297
                        return br;
×
298
                }
299
        }
300

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

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

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

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

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

389
        private class MachineNameId {
390
                int id;
391

392
                String name;
393

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

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

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

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

461
        /**
462
         * Exception thrown when the machine state can't be read from or written to
463
         * a BMP. This includes when a blacklist can't be accessed or when the
464
         * machine state structure can't be read.
465
         *
466
         * @author Donal Fellows
467
         */
468
        public static final class MachineStateException extends RuntimeException {
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 Op(CREATE_SERIAL_READ_REQ, id), Op::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, Op> opGenerator,
604
                        InterruptableConsumer<Op> 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(Op::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 Op(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 Op(CREATE_BLACKLIST_READ, boardId)) {
3✔
723
                        bmpController.triggerSearch(List.of(bmpId));
3✔
724
                        return op.getResult(serial("data", Blacklist.class));
3✔
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 Op(CREATE_BLACKLIST_WRITE, boardId, blacklist)) {
3✔
748
                        bmpController.triggerSearch(List.of(bmpId));
3✔
749
                        op.completed();
3✔
750
                }
751
        }
3✔
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 Op(CREATE_SERIAL_READ_REQ, board.id)) {
3✔
769
                        bmpController.triggerSearch(List.of(board.bmpId));
3✔
770
                        op.completed();
3✔
771
                }
772
                // Can now read out of the DB normally
773
                return findId(board.id).map(b -> b.bmpSerial).orElse(null);
3✔
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
         * @param bmpId
782
         *            The BMP that controls the board.
783
         * @return The board's temperature data.
784
         * @throws DataAccessException
785
         *             If access to the DB fails.
786
         * @throws MachineStateException
787
         *             If the read fails.
788
         * @throws InterruptedException
789
         *             If interrupted.
790
         */
791
        public Optional<ADCInfo> readTemperatureFromMachine(int boardId, int bmpId)
792
                        throws InterruptedException {
793
                try (var op = new Op(CREATE_TEMP_READ_REQ, boardId)) {
×
794
                        bmpController.triggerSearch(List.of(bmpId));
×
795
                        return op.getResult(serial("data", ADCInfo.class));
×
796
                }
797
        }
798

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

817
        /**
818
         * Manages the transactions used to safely talk with the BMP controller.
819
         *
820
         * @author Donal Fellows
821
         */
822
        private final class Op implements AutoCloseable {
823
                private final int op;
824

825
                private final Epoch epoch;
826

827
                private final int boardId;
828

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

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

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

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

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