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

SpiNNakerManchester / JavaSpiNNaker / 11984208506

13 Nov 2024 08:40AM UTC coverage: 37.085% (+0.008%) from 37.077%
11984208506

push

github

web-flow
Merge pull request #1209 from SpiNNakerManchester/fix_bmp_issue

Allow starting with faulty BMPs

10 of 13 new or added lines in 1 file covered. (76.92%)

10 existing lines in 3 files now uncovered.

8777 of 23667 relevant lines covered (37.09%)

1.11 hits per line

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

62.17
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/bmp/BMPController.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.bmp;
17

18
import static java.lang.String.format;
19
import static java.lang.Thread.currentThread;
20
import static java.lang.Thread.sleep;
21
import static java.util.Objects.requireNonNull;
22
import static org.slf4j.LoggerFactory.getLogger;
23
import static uk.ac.manchester.spinnaker.alloc.bmp.NonBootOperation.GET_SERIAL;
24
import static uk.ac.manchester.spinnaker.alloc.bmp.NonBootOperation.READ_BL;
25
import static uk.ac.manchester.spinnaker.alloc.bmp.NonBootOperation.READ_TEMP;
26
import static uk.ac.manchester.spinnaker.alloc.bmp.NonBootOperation.WRITE_BL;
27
import static uk.ac.manchester.spinnaker.alloc.model.JobState.DESTROYED;
28
import static uk.ac.manchester.spinnaker.alloc.model.JobState.QUEUED;
29

30
import java.io.IOException;
31
import java.lang.Thread.UncaughtExceptionHandler;
32
import java.time.Instant;
33
import java.util.ArrayList;
34
import java.util.Collection;
35
import java.util.HashMap;
36
import java.util.LinkedList;
37
import java.util.List;
38
import java.util.Map;
39
import java.util.Optional;
40
import java.util.function.Consumer;
41
import java.util.stream.Collectors;
42

43
import javax.annotation.PostConstruct;
44

45
import org.slf4j.Logger;
46
import org.springframework.beans.factory.ObjectProvider;
47
import org.springframework.beans.factory.annotation.Autowired;
48
import org.springframework.jmx.export.annotation.ManagedResource;
49
import org.springframework.scheduling.TaskScheduler;
50
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
51
import org.springframework.stereotype.Service;
52

53
import com.google.errorprone.annotations.RestrictedApi;
54
import com.google.errorprone.annotations.concurrent.GuardedBy;
55

56
import uk.ac.manchester.spinnaker.alloc.ForTestingOnly;
57
import uk.ac.manchester.spinnaker.alloc.ServiceMasterControl;
58
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.AllocatorProperties;
59
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.TxrxProperties;
60
import uk.ac.manchester.spinnaker.alloc.admin.ReportMailSender;
61
import uk.ac.manchester.spinnaker.alloc.allocator.AllocatorTask;
62
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs;
63
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI;
64
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
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.Direction;
68
import uk.ac.manchester.spinnaker.alloc.model.JobState;
69
import uk.ac.manchester.spinnaker.machine.board.BMPBoard;
70
import uk.ac.manchester.spinnaker.machine.board.BMPCoords;
71
import uk.ac.manchester.spinnaker.machine.board.HasBMPLocation;
72
import uk.ac.manchester.spinnaker.messages.model.ADCInfo;
73
import uk.ac.manchester.spinnaker.messages.model.Blacklist;
74
import uk.ac.manchester.spinnaker.transceiver.ProcessException;
75
import uk.ac.manchester.spinnaker.transceiver.ProcessException.CallerProcessException;
76
import uk.ac.manchester.spinnaker.transceiver.ProcessException.PermanentProcessException;
77
import uk.ac.manchester.spinnaker.transceiver.ProcessException.TransientProcessException;
78
import uk.ac.manchester.spinnaker.transceiver.SpinnmanException;
79
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
80

81
/**
82
 * Manages the BMPs of machines controlled by Spalloc.
83
 *
84
 * @author Donal Fellows
85
 */
86
@Service("bmpController")
87
@ManagedResource("Spalloc:type=BMPController,name=bmpController")
88
public class BMPController extends DatabaseAwareBean {
3✔
89
        private static final Logger log = getLogger(BMPController.class);
3✔
90

91
        @Autowired
92
        private SpallocAPI spallocCore;
93

94
        @Autowired
95
        private ServiceMasterControl serviceControl;
96

97
        @Autowired
98
        private Epochs epochs;
99

100
        @Autowired
101
        private TxrxProperties props;
102

103
        @Autowired
104
        private PhysicalSerialMapping phySerMap;
105

106
        @Autowired
107
        private AllocatorProperties allocProps;
108

109
        @Autowired
110
        private ReportMailSender emailSender;
111

112
        @Autowired
113
        private AllocatorTask allocator;
114

115
        private TaskScheduler scheduler;
116

117
        /**
118
         * Synchronizer for power request access to the database (as otherwise
119
         * deadlocks can occur when multiple transactions try to update the boards
120
         * table).
121
         */
122
        private Object powerDBSync = new Object();
3✔
123

124
        /**
125
         * Map from BMP ID to worker task that handles it.
126
         */
127
        private final Map<Integer, Worker> workers = new HashMap<>();
3✔
128

129
        /**
130
         * Factory for {@linkplain SpiNNakerControl controllers}. Only use via
131
         * {@link #controllerFactory}.
132
         */
133
        @Autowired
134
        private ObjectProvider<SpiNNakerControl> controllerFactoryBean;
135

136
        /**
137
         * Type-safe factory for {@linkplain SpiNNakerControl controllers}.
138
         */
139
        private SpiNNakerControl.Factory controllerFactory;
140

141
        @GuardedBy("this")
142
        private Throwable bmpProcessingException;
143

144
        /**
145
         * An {@link UncaughtExceptionHandler}.
146
         *
147
         * @param thread
148
         *            The thread with the problem.
149
         * @param exception
150
         *            The exception that describes the problem.
151
         */
152
        @UsedInJavadocOnly(UncaughtExceptionHandler.class)
153
        private void handleException(Thread thread, Throwable exception) {
154
                log.error("uncaught exception in BMP worker {}", thread, exception);
×
155
        }
×
156

157
        // ----------------------------------------------------------------
158

159
        @PostConstruct
160
        private void init() {
161
                // Set up scheduler
162
                var sched = new ThreadPoolTaskScheduler();
3✔
163
                scheduler = sched;
3✔
164
                sched.setThreadGroupName("BMP");
3✔
165

166
                controllerFactory = controllerFactoryBean::getObject;
3✔
167
                allocator.setBMPController(this);
3✔
168

169
                // We do the making of workers later in tests
170
                List<Worker> madeWorkers = null;
3✔
171
                if (!serviceControl.isUseDummyBMP()) {
3✔
172
                        madeWorkers = makeWorkers();
×
173
                }
174

175
                // Set the pool size to match the number of workers
176
                if (workers.size() > 1) {
3✔
177
                        sched.setPoolSize(workers.size());
×
178
                }
179

180
                // Launch the scheduler now it is all set up
181
                sched.initialize();
3✔
182

183
                // And now use the scheduler
184
                if (madeWorkers != null) {
3✔
185
                        for (var worker : madeWorkers) {
×
186
                                scheduler.scheduleAtFixedRate(worker, allocProps.getPeriod());
×
187
                        }
×
188
                }
189
        }
3✔
190

191
        private List<Worker> makeWorkers() {
192
                // Make workers
193
                try (var c = getConnection();
3✔
194
                                var getBmps = c.query(GET_ALL_BMPS);
3✔
195
                                var getBoards = c.query(GET_ALL_BMP_BOARDS)) {
3✔
196
                        return c.transaction(false, () -> getBmps.call(row -> {
3✔
197
                                var m = spallocCore.getMachine(row.getString("machine_name"),
3✔
198
                                                true);
199
                                var coords = new BMPCoords(row.getInt("cabinet"),
3✔
200
                                                row.getInt("frame"));
3✔
201
                                var boards = new HashMap<BMPBoard, String>();
3✔
202
                                var bmpId = row.getInt("bmp_id");
3✔
203
                                getBoards.call(r -> {
3✔
204
                                        boards.put(new BMPBoard(r.getInt("board_num")),
3✔
205
                                                        r.getString("address"));
3✔
206
                                        return null;
3✔
207
                                }, bmpId);
3✔
208
                                var worker = new Worker(m.get(), coords, boards, bmpId);
3✔
209
                                workers.put(row.getInt("bmp_id"), worker);
3✔
210
                                return worker;
3✔
211
                        }));
212
                }
213
        }
214

215
        /**
216
         * Trigger the execution of the workers for the given BMPs now.
217
         *
218
         * @param bmps
219
         *            A list of BMPs that have changed.
220
         */
221
        public void triggerSearch(Collection<Integer> bmps) {
222
                for (var b : bmps) {
3✔
223
                        var worker = workers.get(b);
3✔
224
                        if (worker != null) {
3✔
225
                                scheduler.schedule(() -> worker.run(), Instant.now());
3✔
226
                        } else {
227
                                log.error("Could not find worker for BMP {}", b);
3✔
228
                        }
229
                }
3✔
230
        }
3✔
231

232
        /** An action that may throw any of a range of exceptions. */
233
        private interface ThrowingAction {
234
                void act() throws ProcessException, IOException, InterruptedException;
235
        }
236

237
        private abstract class Request {
238
                final int bmpId;
239

240
                private int numTries = 0;
3✔
241

242
                Request(int bmpId) {
3✔
243
                        this.bmpId = bmpId;
3✔
244
                }
3✔
245

246
                /**
247
                 * @return Whether this request may be repeated.
248
                 */
249
                boolean isRepeat() {
250
                        return numTries < props.getPowerAttempts();
3✔
251
                }
252

253
                /**
254
                 * Basic machinery for handling exceptions that arise while performing a
255
                 * BMP action. Runs on a thread that may touch a BMP directly, but which
256
                 * may not touch the database.
257
                 * <p>
258
                 * Only subclasses should use this!
259
                 *
260
                 * @param body
261
                 *            What to attempt.
262
                 * @param onFailure
263
                 *            What to do on failure.
264
                 * @param onServiceRemove
265
                 *            If the exception looks serious, call this to trigger a
266
                 *            board being taken out of service.
267
                 * @return Whether to stop the retry loop.
268
                 * @throws InterruptedException
269
                 *             If interrupted.
270
                 */
271
                final boolean bmpAction(ThrowingAction body,
272
                                Consumer<Exception> onFailure,
273
                                Consumer<PermanentProcessException> onServiceRemove)
274
                                throws InterruptedException {
275
                        boolean isLastTry = numTries++ >= props.getPowerAttempts();
3✔
276
                        Exception exn;
277
                        try {
278
                                body.act();
3✔
279
                                // Exit the retry loop (up the stack); the requests all worked
280
                                return true;
3✔
281
                        } catch (InterruptedException e) {
×
282
                                /*
283
                                 * We were interrupted! This happens when we're shutting down.
284
                                 * Log (because we're in an inconsistent state) and rethrow so
285
                                 * that the outside gets to clean up.
286
                                 */
287
                                log.error("Requests failed on BMP {} because of "
×
288
                                                + "interruption", bmpId, e);
×
289
                                currentThread().interrupt();
×
290
                                throw e;
×
291
                        } catch (TransientProcessException e) {
×
292
                                if (!isLastTry) {
×
293
                                        // Log somewhat gently; we *might* be able to recover...
294
                                        log.warn("Retrying requests on BMP {} after {}: {}",
×
295
                                                        bmpId, props.getProbeInterval(),
×
296
                                                        e.getMessage());
×
297
                                        // Ask for a retry
298
                                        return false;
×
299
                                }
300
                                exn = e;
×
301
                                log.error("Requests failed on BMP {}", bmpId, e);
×
302
                        } catch (PermanentProcessException e) {
×
303
                                log.error("BMP {} on {} is unreachable", e.source, bmpId, e);
×
304
                                onServiceRemove.accept(e);
×
305
                                exn = e;
×
306
                        } catch (CallerProcessException e) {
×
307
                                // This is probably a software bug
308
                                log.error("SW bug talking to BMP {}", bmpId, e);
×
309
                                exn = e;
×
310
                        } catch (ProcessException | IOException | RuntimeException e) {
×
311
                                log.error("Requests failed on BMP {}", bmpId, e);
×
312
                                exn = e;
×
313
                        }
×
314
                        /*
315
                         * Common permanent failure handling case; arrange for taking a
316
                         * board out of service, mark a request as failed, and stop the
317
                         * retry loop.
318
                         */
319
                        onFailure.accept(exn);
×
320
                        return true;
×
321
                }
322

323
                /**
324
                 * Add a report to the database of a problem with a board.
325
                 *
326
                 * @param sql
327
                 *            How to talk to the DB
328
                 * @param boardId
329
                 *            Which board has the problem
330
                 * @param jobId
331
                 *            What job was associated with the problem (if any)
332
                 * @param msg
333
                 *            Information about what the problem was
334
                 */
335
                final void addBoardReport(Connection c, int boardId, Integer jobId,
336
                                String msg) {
337
                        try (var getUser = c.query(GET_USER_DETAILS_BY_NAME);
×
338
                                        var insertBoardReport = c.update(INSERT_BOARD_REPORT)) {
×
339
                                getUser.call1(row -> row.getInt("user_id"),
×
340
                                                allocProps.getSystemReportUser()).ifPresent(
×
341
                                                                userId -> insertBoardReport.call(
×
342
                                                                                boardId, jobId,        msg, userId));
×
343
                        }
344
                }
×
345

346
                /**
347
                 * Marks a board as actually dead, and requests we send email about it.
348
                 *
349
                 * @param sql
350
                 *            How to talk to the DB
351
                 * @param boardId
352
                 *            Which board has the problem
353
                 * @param msg
354
                 *            Information about what the problem was
355
                 * @return Whether we've successfully done a change.
356
                 */
357
                final void markBoardAsDead(Connection c, int boardId, String msg) {
358
                        try (var setFunctioning = c.update(SET_FUNCTIONING_FIELD);
×
359
                                        var findBoardById = c.query(FIND_BOARD_BY_ID)) {
×
360
                                boolean result = setFunctioning.call(false, boardId) > 0;
×
361
                                if (result) {
×
362
                                        findBoardById.call1(row -> {
×
363
                                                var ser = row.getString("physical_serial_id");
×
364
                                                if (ser == null) {
×
365
                                                        ser = "<UNKNOWN>";
×
366
                                                }
367
                                                var fullMessage = format(
×
368
                                                                "Marked board at %d,%d,%d of %s (serial: %s) "
369
                                                                                + "as dead: %s",
370
                                                                row.getInt("x"), row.getInt("y"),
×
371
                                                                row.getInt("z"), row.getString("machine_name"),
×
372
                                                                ser, msg);
373
                                                emailSender.sendServiceMail(fullMessage);
×
374
                                                return null;
×
375
                                        }, boardId);
×
376
                                }
377
                        }
378
                }
×
379

380
                boolean processRequest(SpiNNakerControl control) {
381
                        while (isRepeat()) {
3✔
382
                                try {
383
                                        if (tryProcessRequest(control)) {
3✔
384
                                                return true;
3✔
385
                                        }
386
                                        sleep(props.getProbeInterval().toMillis());
×
387
                                } catch (InterruptedException e) {
×
388
                                        // If this happens, just cancel the transaction;
389
                                        // when we come back, all things will be redone.
390
                                        throw new RuntimeException(e);
×
391
                                }
×
392
                        }
393
                        return false;
×
394
                }
395

396
                abstract boolean tryProcessRequest(SpiNNakerControl control)
397
                                throws InterruptedException;
398
        }
399

400
        /**
401
         * Describes a request to modify the power status of a collection of boards.
402
         * The boards must be on a single machine and must all be assigned to a
403
         * single job.
404
         * <p>
405
         * This is the message that is sent from the main thread to the per-BMP
406
         * worker threads.
407
         *
408
         * @author Donal Fellows
409
         */
410
        private final class PowerRequest extends Request {
411
                private final List<BMPBoard> powerOnBoards = new ArrayList<>();
3✔
412

413
                private final List<BMPBoard> powerOffBoards = new ArrayList<>();
3✔
414

415
                private final List<Link> linkRequests = new ArrayList<>();
3✔
416

417
                private final int jobId;
418

419
                private final JobState from;
420

421
                private final JobState to;
422

423
                private final List<Integer> changeIds = new ArrayList<>();
3✔
424

425
                private final Map<Integer, Integer> boardToId = new HashMap<>();
3✔
426

427
                /**
428
                 * Create a request.
429
                 *
430
                 * @param sql
431
                 *            How to access the database.
432
                 * @param machine
433
                 *            What machine are the boards on? <em>Must not</em> be
434
                 *            {@code null}.
435
                 * @param powerOn
436
                 *            What boards (by DB ID) are to be powered on? May be
437
                 *            {@code null}; that's equivalent to the empty list.
438
                 * @param powerOff
439
                 *            What boards (by DB ID) are to be powered off? May be
440
                 *            {@code null}; that's equivalent to the empty list.
441
                 * @param links
442
                 *            Any link power control requests. By default, links are on
443
                 *            if their board is on and they are connected; it is
444
                 *            <em>useful and relevant</em> to modify the power state of
445
                 *            links on the periphery of an allocation. May be
446
                 *            {@code null}; that's equivalent to the empty list.
447
                 * @param jobId
448
                 *            For what job is this?
449
                 * @param from
450
                 *            What state is the job moving from?
451
                 * @param to
452
                 *            What state is the job moving to?
453
                 * @param changeIds
454
                 *            The DB ids that describe the change, so we can update
455
                 *            those records.
456
                 * @param idToBoard
457
                 *            How to get the physical ID of a board from its database ID
458
                 */
459
                PowerRequest(int bmpId, int jobId, JobState from, JobState to,
460
                                List<PowerChange> powerChanges) {
3✔
461
                        super(bmpId);
3✔
462
                        for (var change : powerChanges) {
3✔
463
                                if (change.power) {
3✔
464
                                        powerOnBoards.add(new BMPBoard(change.boardNum));
3✔
465
                                } else {
466
                                        powerOffBoards.add(new BMPBoard(change.boardNum));
3✔
467
                                }
468
                                change.offLinks.stream().forEach(link ->
3✔
469
                                                linkRequests.add(new Link(change.boardNum, link)));
3✔
470
                                changeIds.add(change.changeId);
3✔
471
                                boardToId.put(change.boardNum, change.boardId);
3✔
472
                        }
3✔
473
                        this.jobId = jobId;
3✔
474
                        this.from = from;
3✔
475
                        this.to = to;
3✔
476
                }
3✔
477

478
                /**
479
                 * Change the power state of boards in this request.
480
                 *
481
                 * @param controllers
482
                 *            How to actually communicate with the machine
483
                 * @throws ProcessException
484
                 *             If the transceiver chokes
485
                 * @throws InterruptedException
486
                 *             If interrupted
487
                 * @throws IOException
488
                 *             If network I/O fails
489
                 */
490
                void changeBoardPowerState(SpiNNakerControl controller)
491
                                throws ProcessException, InterruptedException, IOException {
492

493
                        // Send any power on commands
494
                        if (!powerOnBoards.isEmpty()) {
3✔
495
                                controller.powerOnAndCheck(powerOnBoards);
3✔
496
                        }
497

498
                        // Process perimeter link requests next
499
                        for (var linkReq : linkRequests) {
3✔
500
                                // Set the link state, as required
501
                                controller.setLinkOff(linkReq);
3✔
502
                        }
3✔
503

504
                        // Finally send any power off commands
505
                        if (!powerOffBoards.isEmpty()) {
3✔
506
                                controller.powerOff(powerOffBoards);
3✔
507
                        }
508
                }
3✔
509

510
                /**
511
                 * Handles the database changes after a set of changes to a BMP complete
512
                 * successfully. We will move the job to the state it supposed to be in.
513
                 *
514
                 * @param sql
515
                 *            How to access the DB
516
                 * @return Whether the state of boards or jobs has changed.
517
                 */
518
                private void done() {
519
                        try (var c = getConnection();
3✔
520
                                        var deallocateBoards = c.update(DEALLOCATE_BMP_BOARDS_JOB);
3✔
521
                                        var deleteChange = c.update(FINISHED_PENDING);
3✔
522
                                        var setBoardPowerOn = c.update(SET_BOARD_POWER_ON);
3✔
523
                                        var setBoardPowerOff = c.update(SET_BOARD_POWER_OFF)) {
3✔
524
                                c.transaction(() -> {
3✔
525
                                        int turnedOn = powerOnBoards.stream().map(this::getBoardId)
3✔
526
                                                        .mapToInt(setBoardPowerOn::call).sum();
3✔
527
                                        int turnedOff =
3✔
528
                                                        powerOffBoards.stream().map(this::getBoardId)
3✔
529
                                                                        .mapToInt(setBoardPowerOff::call).sum();
3✔
530

531
                                        if (to == DESTROYED || to == QUEUED) {
3✔
532
                                                /*
533
                                                 * Need to mark the boards as not allocated; can't do
534
                                                 * that until they've been switched off.
535
                                                 */
536
                                                deallocateBoards.call(jobId, bmpId);
3✔
537
                                        }
538
                                        int completed = changeIds.stream().mapToInt(
3✔
539
                                                        deleteChange::call).sum();
3✔
540

541
                                        log.debug("BMP ACTION SUCCEEDED ({}:{}->{}): on:{} off:{} "
3✔
542
                                                        + "completed: {}",
543
                                                        jobId, from, to, turnedOn, turnedOff, completed);
3✔
544
                                });
3✔
545
                        }
546

547
                        // Tell the allocator something has happened
548
                        allocator.updateJob(jobId, from, to);
3✔
549
                }
3✔
550

551
                /**
552
                 * Handles the database changes after a set of changes to a BMP complete
553
                 * with a failure. We will roll back the job state to what it was
554
                 * before.
555
                 *
556
                 * @param sql
557
                 *            How to access the DB
558
                 * @return Whether the state of boards or jobs has changed.
559
                 */
560
                private void failed() {
561
                        try (var c = getConnection();
×
562
                                        var deallocateBoards = c.update(DEALLOCATE_BMP_BOARDS_JOB);
×
563
                                        var deleteChange = c.update(FINISHED_PENDING);
×
564
                                        var errorChange = c.update(ERROR_PENDING);
×
565
                                        var setBoardPowerOff = c.update(SET_BOARD_POWER_OFF)) {
×
566
                                c.transaction(() -> {
×
567
                                        // We should mark the boards as off
568
                                        int turnedOff =
×
569
                                                        powerOffBoards.stream().map(this::getBoardId)
×
570
                                                                        .mapToInt(setBoardPowerOff::call).sum();
×
571

572
                                        // ... even those that we should be powering on ...
573
                                        turnedOff +=
×
574
                                                        powerOnBoards.stream().map(this::getBoardId)
×
575
                                                                        .mapToInt(setBoardPowerOff::call).sum();
×
576

577
                                        // If we are going to queued or destroyed, we can just
578
                                        // ignore the error as we will reallocate anyway
579
                                        int completed = 0;
×
580
                                        if (to == DESTROYED || to == QUEUED) {
×
581
                                                // Need to mark the boards as not allocated; slightly
582
                                                // dodgy since they might still be on, but not a lot
583
                                                // we can do about it!
584
                                                deallocateBoards.call(jobId, bmpId);
×
585
                                                completed = changeIds.stream().mapToInt(
×
586
                                                                deleteChange::call).sum();
×
587
                                        } else {
588

589
                                                // If we are going to READY, we must mark changes as
590
                                                // failed to make sure we don't think we are done!
591
                                                completed = changeIds.stream().mapToInt(
×
592
                                                                errorChange::call).sum();
×
593
                                        }
594

595
                                        log.debug(
×
596
                                                        "BMP ACTION FAILED on {} ({}:{}->{}) off:{} "
597
                                                        + " completed {}",
598
                                                        bmpId, jobId, from, to, turnedOff, completed);
×
599
                                });
×
600
                        }
601
                        // Tell the allocator something has happened
602
                        allocator.updateJob(jobId, from, to);
×
603
                }
×
604

605
                /**
606
                 * Process an action to power on or off a set of boards. Runs on a
607
                 * thread that may touch a BMP directly, but which may not touch the
608
                 * database.
609
                 *
610
                 * @param controller
611
                 *            How to actually reach the BMPs.
612
                 * @return Whether this action has "succeeded" and shouldn't be retried.
613
                 * @throws InterruptedException
614
                 *             If interrupted.
615
                 */
616
                @Override
617
                boolean tryProcessRequest(SpiNNakerControl controller)
618
                                throws InterruptedException {
619
                        boolean ok = bmpAction(() -> {
3✔
620
                                changeBoardPowerState(controller);
3✔
621
                                // We want to ensure the lead board is alive
622
                                if (!serviceControl.isUseDummyBMP()) {
3✔
623
                                        // Don't bother with pings when the dummy is enabled
624
                                        controller.ping(powerOnBoards);
×
625
                                }
626
                                synchronized (powerDBSync) {
3✔
627
                                        done();
3✔
628
                                }
3✔
629
                        }, e -> {
3✔
630
                                synchronized (powerDBSync) {
×
631
                                        failed();
×
632
                                }
×
633
                                synchronized (BMPController.this) {
×
634
                                        bmpProcessingException = e;
×
635
                                }
×
636
                        }, ppe -> {
×
637
                                synchronized (powerDBSync) {
×
638
                                        badBoard(ppe);
×
639
                                }
×
640
                        });
×
641
                        return ok;
3✔
642
                }
643

644
                @Override
645
                public String toString() {
646
                        var sb = new StringBuilder("PowerRequest(for=")
×
647
                                        .append(bmpId);
×
648
                        sb.append(";on=").append(powerOnBoards);
×
649
                        sb.append(",off=").append(powerOffBoards);
×
650
                        sb.append(",links=").append(linkRequests);
×
651
                        return sb.append(")").toString();
×
652
                }
653

654
                private static final String REPORT_MSG =
655
                                "board was not reachable when trying to power it: ";
656

657
                /**
658
                 * When a BMP is unroutable, we must tell the alloc engine to pick
659
                 * somewhere else, and we should mark the board as out of service too;
660
                 * it's never going to work so taking it out right away is the only sane
661
                 * plan. We also need to nuke the planned changes. Retrying is bad.
662
                 *
663
                 * @param failure
664
                 *            The failure message.
665
                 * @return Whether the state of boards or jobs has changed.
666
                 */
667
                private void badBoard(ProcessException failure) {
668
                        try (var c = getConnection()) {
×
669
                                c.transaction(() -> {
×
670
                                        getBoardId(failure.source).ifPresent(boardId -> {
×
671
                                                // Mark the board as dead right now
672
                                                markBoardAsDead(c, boardId, REPORT_MSG + failure);
×
673
                                                // Add a report if we can
674
                                                addBoardReport(c, boardId, jobId, REPORT_MSG + failure);
×
675
                                        });
×
676
                                });
×
677
                        }
678
                }
×
679

680
                /**
681
                 * Given a board address, get the ID that it corresponds to. Reverses
682
                 * {@link #idToBoard}.
683
                 *
684
                 * @param addr
685
                 *            The board address.
686
                 * @return The ID, if one can be found.
687
                 */
688
                private Optional<Integer> getBoardId(HasBMPLocation addr) {
689
                        return Optional.ofNullable(boardToId.get(addr.getBoard()));
×
690
                }
691

692
                private Integer getBoardId(BMPBoard board) {
693
                        return boardToId.get(board.board);
3✔
694
                }
695
        }
696

697
        /**
698
         * A request to read or write information on a BMP. Includes blacklists,
699
         * serial numbers, temperature data, etc.
700
         *
701
         * @author Donal Fellows
702
         */
703
        private final class BoardRequest extends Request {
704
                private final NonBootOperation op;
705

706
                private final int opId;
707

708
                private final int boardId;
709

710
                private final BMPCoords bmp;
711

712
                private final BMPBoard board;
713

714
                private final String bmpSerialId;
715

716
                private final Blacklist blacklist;
717

718
                private final int machineId;
719

720
                private BoardRequest(int bmpId, NonBootOperation op, Row row) {
3✔
721
                        super(bmpId);
3✔
722
                        this.op = op;
3✔
723
                        opId = row.getInt("op_id");
3✔
724
                        boardId = row.getInt("board_id");
3✔
725
                        bmp = new BMPCoords(row.getInt("cabinet"), row.getInt("frame"));
3✔
726
                        board = new BMPBoard(row.getInt("board_num"));
3✔
727
                        if (op == WRITE_BL) {
3✔
728
                                blacklist = row.getSerial("data", Blacklist.class);
3✔
729
                        } else {
730
                                blacklist = null;
3✔
731
                        }
732
                        bmpSerialId = row.getString("bmp_serial_id");
3✔
733
                        machineId = row.getInt("machine_id");
3✔
734
                }
3✔
735

736
                /** The serial number actually read from the board. */
737
                private String readSerial;
738

739
                /**
740
                 * Access the DB to store the serial number information that we
741
                 * retrieved. A transaction should already be held.
742
                 *
743
                 * @param c
744
                 *            How to access the DB
745
                 * @return Whether we've changed anything
746
                 */
747
                private void recordSerialIds(Connection c) {
748
                        try (var setBoardSerialIds = c.update(SET_BOARD_SERIAL_IDS)) {
3✔
749
                                setBoardSerialIds.call(boardId, readSerial,
3✔
750
                                                phySerMap.getPhysicalId(readSerial));
3✔
751
                        }
752
                }
3✔
753

754
                /**
755
                 * Access the DB to mark the read request as successful and store the
756
                 * blacklist that was read. A transaction should already be held.
757
                 *
758
                 * @param c
759
                 *            How to access the DB
760
                 * @param readBlacklist
761
                 *            The blacklist that was read
762
                 * @return Whether we've changed anything
763
                 */
764
                private void doneReadBlacklist(Connection c, Blacklist readBlacklist) {
765
                        try (var completed = c.update(COMPLETED_BOARD_INFO_READ)) {
3✔
766
                                log.debug("Completing blacklist read opId {}", opId);
3✔
767
                                completed.call(readBlacklist, opId);
3✔
768
                        }
769
                }
3✔
770

771
                /**
772
                 * Access the DB to mark the write request as successful. A transaction
773
                 * should already be held.
774
                 *
775
                 * @param c
776
                 *            How to access the DB
777
                 * @return Whether we've changed anything
778
                 */
779
                private void doneWriteBlacklist(Connection c) {
780
                        try (var completed = c.update(COMPLETED_BLACKLIST_WRITE)) {
3✔
781
                                completed.call(opId);
3✔
782
                        }
783
                }
3✔
784

785
                /**
786
                 * Access the DB to mark the read request as successful; the actual
787
                 * store of the serial data is elsewhere
788
                 * ({@link #recordSerialIds(Connection)}). A transaction should already
789
                 * be held.
790
                 *
791
                 * @param c
792
                 *            How to access the DB
793
                 * @return Whether we've changed anything
794
                 */
795
                private void doneReadSerial(Connection c) {
796
                        try (var completed = c.update(COMPLETED_GET_SERIAL_REQ)) {
3✔
797
                                completed.call(opId);
3✔
798
                        }
799
                }
3✔
800

801
                /**
802
                 * Access the DB to mark the read request as successful and store the
803
                 * ADC info that was read. A transaction should be held.
804
                 *
805
                 * @param c
806
                 *            The database connection.
807
                 */
808
                private void doneReadTemps(Connection c, ADCInfo adcInfo) {
809
                        try (var completed = c.update(COMPLETED_BOARD_INFO_READ)) {
×
810
                                log.debug("Completing temperature read opId {}", opId);
×
811
                                completed.call(adcInfo, opId);
×
812
                        }
813
                }
×
814

815
                /**
816
                 * Access the DB to mark the request as failed and store the exception.
817
                 *
818
                 * @param exn
819
                 *            The exception that caused the failure.
820
                 * @return Whether we've changed anything
821
                 */
822
                private void failed(Exception exn) {
823
                        try (var c = getConnection();
×
824
                                        var failed = c.update(FAILED_BLACKLIST_OP)) {
×
825
                                c.transaction(() -> failed.call(exn, opId));
×
826
                        }
827
                }
×
828

829
                private static final String REPORT_MSG =
830
                                "board was not reachable when trying to access its blacklist: ";
831

832
                /**
833
                 * Access the DB to mark a board as out of service.
834
                 *
835
                 * @param exn
836
                 *            The exception that caused the failure.
837
                 * @return Whether we've changed anything
838
                 */
839
                void takeOutOfService(Exception exn) {
840
                        try (var c = getConnection()) {
×
841
                                c.transaction(() -> {
×
842
                                        addBoardReport(c, boardId, null, REPORT_MSG + exn);
×
843
                                        markBoardAsDead(c, boardId, REPORT_MSG + exn);
×
844
                                });
×
845
                        }
846
                }
×
847

848
                /**
849
                 * Process an action to work with a blacklist or serial number. Runs on
850
                 * a thread that may touch a BMP directly, but which may not touch the
851
                 * database.
852
                 *
853
                 * @param controller
854
                 *            How to actually reach the BMP.
855
                 * @return Whether this action has "succeeded" and shouldn't be retried.
856
                 * @throws InterruptedException
857
                 *             If interrupted.
858
                 */
859
                @Override
860
                boolean tryProcessRequest(SpiNNakerControl controller)
861
                                throws InterruptedException {
862
                        return bmpAction(() -> {
3✔
863
                                switch (op) {
3✔
864
                                case WRITE_BL:
865
                                        writeBlacklist(controller);
3✔
866
                                        break;
3✔
867
                                case READ_BL:
868
                                        readBlacklist(controller);
3✔
869
                                        break;
3✔
870
                                case GET_SERIAL:
871
                                        readSerial(controller);
3✔
872
                                        break;
3✔
873
                                case READ_TEMP:
874
                                        readTemps(controller);
×
875
                                        break;
×
876
                                default:
877
                                        throw new IllegalArgumentException();
×
878
                                }
879
                                epochs.blacklistChanged(boardId);
3✔
880
                                epochs.machineChanged(machineId);
3✔
881
                        }, e -> {
3✔
882
                                failed(e);
×
883
                                epochs.blacklistChanged(boardId);
×
884
                                epochs.machineChanged(machineId);
×
885
                        }, ppe -> {
×
886
                                takeOutOfService(ppe);
×
887
                        });
×
888
                }
889

890
                /**
891
                 * Process an action to read a blacklist.
892
                 *
893
                 * @param controller
894
                 *            How to actually reach the BMP.
895
                 * @throws InterruptedException
896
                 *             If interrupted.
897
                 * @throws IOException
898
                 *             If the network is unhappy.
899
                 * @throws ProcessException
900
                 *             If the BMP rejects a message.
901
                 */
902
                private void readBlacklist(SpiNNakerControl controller)
903
                                throws InterruptedException, ProcessException, IOException {
904
                        readSerial = controller.readSerial(board);
3✔
905
                        if (bmpSerialId != null && !bmpSerialId.equals(readSerial)) {
3✔
906
                                /*
907
                                 * Doesn't match; WARN but keep going; hardware may just be
908
                                 * remapped behind our back.
909
                                 */
910
                                log.warn(
×
911
                                                "blacklist read mismatch: expected serial ID '{}' "
912
                                                                + "not equal to actual serial ID '{}'",
913
                                                bmpSerialId, readSerial);
914
                        }
915
                        var readBlacklist = controller.readBlacklist(board);
3✔
916
                        try (var c = getConnection()) {
3✔
917
                                c.transaction(() -> {
3✔
918
                                        recordSerialIds(c);
3✔
919
                                        doneReadBlacklist(c, readBlacklist);
3✔
920
                                });
3✔
921
                        }
922
                }
3✔
923

924
                /**
925
                 * Process an action to write a blacklist.
926
                 *
927
                 * @param controller
928
                 *            How to actually reach the BMP.
929
                 * @throws InterruptedException
930
                 *             If interrupted.
931
                 * @throws IOException
932
                 *             If the network is unhappy.
933
                 * @throws ProcessException
934
                 *             If the BMP rejects a message.
935
                 * @throws IllegalStateException
936
                 *             If the operation is applied to a board other than the one
937
                 *             that it is expected to apply to.
938
                 */
939
                private void writeBlacklist(SpiNNakerControl controller)
940
                                throws InterruptedException, ProcessException, IOException {
941
                        readSerial = controller.readSerial(board);
3✔
942
                        if (bmpSerialId != null && !bmpSerialId.equals(readSerial)) {
3✔
943
                                // Doesn't match, so REALLY unsafe to keep going!
944
                                throw new IllegalStateException(format(
×
945
                                                "aborting blacklist write: expected serial ID '%s' "
946
                                                                + "not equal to actual serial ID '%s'",
947
                                                bmpSerialId, readSerial));
948
                        }
949
                        controller.writeBlacklist(board, requireNonNull(blacklist));
3✔
950
                        try (var c = getConnection()) {
3✔
951
                                c.transaction(() -> doneWriteBlacklist(c));
3✔
952
                        }
953
                }
3✔
954

955
                /**
956
                 * Process an action to read the serial number from a BMP.
957
                 *
958
                 * @param controller
959
                 *            How to actually reach the BMP.
960
                 * @throws InterruptedException
961
                 *             If interrupted.
962
                 * @throws IOException
963
                 *             If the network is unhappy
964
                 * @throws ProcessException
965
                 *             If the BMP rejects a message.
966
                 */
967
                private void readSerial(SpiNNakerControl controller)
968
                                throws InterruptedException, ProcessException, IOException {
969
                        readSerial = controller.readSerial(board);
3✔
970
                        try (var c = getConnection()) {
3✔
971
                                c.transaction(() -> {
3✔
972
                                        recordSerialIds(c);
3✔
973
                                        doneReadSerial(c);
3✔
974
                                });
3✔
975
                        }
976
                }
3✔
977

978
                /**
979
                 * Process an action to read some temperature data.
980
                 *
981
                 * @param controller
982
                 *            How to actually reach the BMP.
983
                 * @throws InterruptedException
984
                 *             If interrupted.
985
                 * @throws IOException
986
                 *             If the network is unhappy.
987
                 * @throws ProcessException
988
                 *             If the BMP rejects a message.
989
                 */
990
                private void readTemps(SpiNNakerControl controller)
991
                                throws InterruptedException, ProcessException, IOException {
992
                        var adcInfo = controller.readTemp(board);
×
993
                        try (var c = getConnection()) {
×
994
                                c.transaction(() -> doneReadTemps(c, adcInfo));
×
995
                        }
996
                }
×
997

998
                @Override
999
                public String toString() {
1000
                        var sb = new StringBuilder("BoardRequest(for ");
×
1001
                        sb.append("bmp=").append(bmp);
×
1002
                        sb.append(",board=").append(boardId);
×
1003
                        sb.append(",op=").append(op);
×
1004
                        return sb.append(")").toString();
×
1005
                }
1006
        }
1007

1008
        private class PowerChange {
1009
                final Integer changeId;
1010

1011
                final int jobId;
1012

1013
                final Integer boardId;
1014

1015
                final Integer boardNum;
1016

1017
                final Instant powerOffTime;
1018

1019
                final boolean power;
1020

1021
                final JobState from;
1022

1023
                final JobState to;
1024

1025
                final List<Direction> offLinks;
1026

1027
                PowerChange(Row row) {
3✔
1028
                        changeId = row.getInteger("change_id");
3✔
1029
                        jobId = row.getInt("job_id");
3✔
1030
                        boardId = row.getInteger("board_id");
3✔
1031
                        boardNum = row.getInteger("board_num");
3✔
1032
                        power = row.getBoolean("power");
3✔
1033
                        from = row.getEnum("from_state", JobState.class);
3✔
1034
                        to = row.getEnum("to_state", JobState.class);
3✔
1035
                        offLinks = List.of(Direction.values()).stream().filter(
3✔
1036
                                        link -> !row.getBoolean(link.columnName)).collect(
3✔
1037
                                                        Collectors.toList());
3✔
1038
                        Instant powerOff = row.getInstant("power_off_timestamp");
3✔
1039
                        if (powerOff == null) {
3✔
1040
                                powerOff = Instant.EPOCH;
3✔
1041
                        }
1042
                        powerOffTime = powerOff;
3✔
1043
                }
3✔
1044

1045
                boolean isSameJob(PowerChange p) {
1046
                        return p.jobId == jobId && p.from == from && p.to == to;
×
1047
                }
1048
        }
1049

1050
        // ----------------------------------------------------------------
1051
        // WORKER IMPLEMENTATION
1052

1053
        /** A worker of a given BMP. */
1054
        private final class Worker implements Runnable {
1055
                /** What are we controlling? */
1056
                private SpiNNakerControl control;
1057

1058
                private final SpallocAPI.Machine machine;
1059

1060
                private final BMPCoords coords;
1061

1062
                private final Map<BMPBoard, String> boards;
1063

1064
                /** Which boards are we looking at? */
1065
                private final int bmpId;
1066

1067
                Worker(SpallocAPI.Machine machine, BMPCoords coords,
1068
                                Map<BMPBoard, String> boards, int bmpId) {
3✔
1069
                        this.machine = machine;
3✔
1070
                        this.coords = coords;
3✔
1071
                        this.boards = boards;
3✔
1072
                        this.bmpId = bmpId;
3✔
1073

1074
                        log.debug("Created worker for boards {}", bmpId);
3✔
1075
                }
3✔
1076

1077
                private SpiNNakerControl getControl() {
1078
                        if (control == null) {
3✔
1079
                                try {
1080
                                        control = controllerFactory.create(machine, coords, boards);
3✔
NEW
1081
                                } catch (Exception e) {
×
NEW
1082
                                        log.error("Could not create control for BMP '{}'",
×
NEW
1083
                                                        bmpId, e);
×
1084
                                }
3✔
1085
                        }
1086
                        return control;
3✔
1087
                }
1088

1089
                /**
1090
                 * Periodically call to update, or trigger externally.
1091
                 */
1092
                @Override
1093
                public synchronized void run() {
1094
                        log.trace("Searching for changes on BMP {}", bmpId);
3✔
1095

1096
                        try {
1097
                                var changes = getRequestedOperations();
3✔
1098
                                for (var change : changes) {
3✔
1099
                                        change.processRequest(getControl());
3✔
1100
                                }
3✔
1101
                        } catch (Exception e) {
×
1102
                                log.error("unhandled exception for BMP '{}'", bmpId, e);
×
1103
                        }
3✔
1104
                }
3✔
1105

1106
                private boolean waitedLongEnough(PowerChange change) {
1107
                        // Power off can be done any time
1108
                        if (!change.power) {
3✔
1109
                                return true;
3✔
1110
                        }
1111

1112
                        // Power on should wait until a time after last off
1113
                        Instant powerOnTime = change.powerOffTime.plus(
3✔
1114
                                        props.getOffWaitTime());
3✔
1115
                        return powerOnTime.isBefore(Instant.now());
3✔
1116
                }
1117

1118
                /**
1119
                 * Get the things that we want the worker to do. <em>Be very
1120
                 * careful!</em> Because this necessarily involves the database, this
1121
                 * must not touch the BMP handle as those operations take a long time
1122
                 * and we absolutely must not have a transaction open at the same time.
1123
                 *
1124
                 * @return List of operations to perform.
1125
                 */
1126
                private List<Request> getRequestedOperations() {
1127
                        var requests = new ArrayList<Request>();
3✔
1128
                        try (var c = getConnection();
3✔
1129
                                        var getPowerRequests = c.query(GET_CHANGES);
3✔
1130
                                        var getBlacklistReads = c.query(GET_BLACKLIST_READS);
3✔
1131
                                        var getBlacklistWrites = c.query(GET_BLACKLIST_WRITES);
3✔
1132
                                        var getReadSerialInfos = c.query(GET_SERIAL_INFO_REQS);
3✔
1133
                                        var getReadTemps = c.query(GET_TEMP_INFO_REQS)) {
3✔
1134
                                c.transaction(false, () -> {
3✔
1135
                                        // Batch power requests by job
1136
                                        var powerChanges = new LinkedList<>(
3✔
1137
                                                        getPowerRequests.call(PowerChange::new, bmpId));
3✔
1138
                                        while (!powerChanges.isEmpty()) {
3✔
1139
                                                var change = powerChanges.poll();
3✔
1140
                                                var jobChanges = new ArrayList<>(List.of(change));
3✔
1141
                                                var canDoNow = waitedLongEnough(change);
3✔
1142
                                                while (!powerChanges.isEmpty()
3✔
1143
                                                                && change.isSameJob(powerChanges.peek())) {
×
1144
                                                        canDoNow &= waitedLongEnough(powerChanges.peek());
×
1145
                                                        jobChanges.add(powerChanges.poll());
×
1146
                                                }
1147
                                                if (!jobChanges.isEmpty() && canDoNow) {
3✔
1148
                                                        log.debug("Running job changes {}", jobChanges);
3✔
1149
                                                        requests.add(new PowerRequest(bmpId, change.jobId,
3✔
1150
                                                                        change.from, change.to, jobChanges));
1151
                                                }
1152
                                        }
3✔
1153

1154
                                        // Leave these until quiet
1155
                                        if (requests.isEmpty()) {
3✔
1156
                                                requests.addAll(getBlacklistReads.call(
3✔
1157
                                                                row -> new BoardRequest(bmpId, READ_BL, row),
3✔
1158
                                                                bmpId));
3✔
1159
                                        }
1160
                                        if (requests.isEmpty()) {
3✔
1161
                                                requests.addAll(getBlacklistWrites.call(
3✔
1162
                                                                row -> new BoardRequest(bmpId, WRITE_BL, row),
3✔
1163
                                                                bmpId));
3✔
1164
                                                requests.addAll(getReadSerialInfos.call(
3✔
1165
                                                                row -> new BoardRequest(bmpId, GET_SERIAL, row),
3✔
1166
                                                                bmpId));
3✔
1167
                                                requests.addAll(getReadTemps.call(
3✔
1168
                                                                row -> new BoardRequest(bmpId, READ_TEMP, row),
×
1169
                                                                bmpId));
3✔
1170
                                        }
1171
                                });
3✔
1172
                        } catch (Exception e) {
×
1173
                                log.error("unhandled exception for BMP '{}'", bmpId, e);
×
1174
                        }
3✔
1175
                        return requests;
3✔
1176
                }
1177
        }
1178

1179
        /**
1180
         * The testing interface.
1181
         *
1182
         * @hidden
1183
         */
1184
        @ForTestingOnly
1185
        public interface TestAPI {
1186
                /**
1187
                 * Ensure things are set up after a database change that updates the
1188
                 * BMPs in the system.
1189
                 */
1190
                void prepare();
1191

1192
                /**
1193
                 * The core of the scheduler.
1194
                 *
1195
                 * @param millis
1196
                 *            How many milliseconds to sleep before doing a rerun of the
1197
                 *            scheduler. If zero (or less), only one run will be done.
1198
                 * @param bmps
1199
                 *            The BMPs to be updated.
1200
                 * @throws IOException
1201
                 *             If talking to the network fails
1202
                 * @throws SpinnmanException
1203
                 *             If a BMP sends an error back
1204
                 * @throws InterruptedException
1205
                 *             If the wait for workers to spawn fails.
1206
                 */
1207
                void processRequests(long millis, Collection<Integer> bmps)
1208
                                throws IOException, SpinnmanException, InterruptedException;
1209

1210
                /**
1211
                 * The core of the scheduler. Will process for all known BMPs.
1212
                 *
1213
                 * @param millis
1214
                 *            How many milliseconds to sleep before doing a rerun of the
1215
                 *            scheduler. If zero (or less), only one run will be done.
1216
                 * @throws IOException
1217
                 *             If talking to the network fails
1218
                 * @throws SpinnmanException
1219
                 *             If a BMP sends an error back
1220
                 * @throws InterruptedException
1221
                 *             If the wait for workers to spawn fails.
1222
                 */
1223
                void processRequests(long millis)
1224
                                throws IOException, SpinnmanException, InterruptedException;
1225

1226
                /**
1227
                 * Get the last BMP exception.
1228
                 *
1229
                 * @return The exception.
1230
                 */
1231
                Throwable getBmpException();
1232

1233
                /**
1234
                 * Clear the last BMP exception.
1235
                 */
1236
                void clearBmpException();
1237
        }
1238

1239
        /**
1240
         * @return The test interface.
1241
         * @deprecated This interface is just for testing.
1242
         * @hidden
1243
         */
1244
        @ForTestingOnly
1245
        @RestrictedApi(explanation = "just for testing", link = "index.html",
1246
                        allowedOnPath = ".*/src/test/java/.*")
1247
        @Deprecated
1248
        public final TestAPI getTestAPI() {
1249
                ForTestingOnly.Utils.checkForTestClassOnStack();
3✔
1250
                return new TestAPI() {
3✔
1251
                        @Override
1252
                        public void prepare() {
1253
                                makeWorkers();
3✔
1254
                        }
3✔
1255

1256
                        @Override
1257
                        public void processRequests(long millis, Collection<Integer> bmps)
1258
                                        throws IOException, SpinnmanException,
1259
                                        InterruptedException {
1260
                                /*
1261
                                 * Runs twice because it takes two cycles to fully process a
1262
                                 * request.
1263
                                 */
1264
                                triggerSearch(bmps);
3✔
1265
                                if (millis > 0) {
3✔
1266
                                        Thread.sleep(millis);
3✔
1267
                                        triggerSearch(bmps);
3✔
1268
                                }
1269
                        }
3✔
1270

1271
                        @Override
1272
                        public void processRequests(long millis) throws IOException,
1273
                                        SpinnmanException, InterruptedException {
1274
                                processRequests(millis, workers.keySet());
3✔
1275
                        }
3✔
1276

1277
                        @Override
1278
                        public Throwable getBmpException() {
1279
                                synchronized (BMPController.this) {
3✔
1280
                                        return bmpProcessingException;
3✔
1281
                                }
1282
                        }
1283

1284
                        @Override
1285
                        public void clearBmpException() {
1286
                                synchronized (BMPController.this) {
3✔
1287
                                        bmpProcessingException = null;
3✔
1288
                                }
3✔
1289
                        }
3✔
1290
                };
1291
        }
1292
}
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