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

SpiNNakerManchester / JavaSpiNNaker / 14657108701

24 Apr 2025 03:30PM UTC coverage: 38.241% (-0.004%) from 38.245%
14657108701

push

github

web-flow
Merge pull request #1225 from SpiNNakerManchester/fix_tooltip

Fix tooltip

9187 of 24024 relevant lines covered (38.24%)

1.14 hits per line

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

78.86
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/compat/V1TaskImpl.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.compat;
17

18
import static java.util.Objects.isNull;
19
import static java.util.Objects.nonNull;
20
import static java.util.stream.Collectors.toList;
21
import static java.util.stream.Collectors.toMap;
22
import static org.apache.commons.lang3.StringUtils.isAsciiPrintable;
23
import static org.slf4j.LoggerFactory.getLogger;
24
import static uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateBoard.triad;
25
import static uk.ac.manchester.spinnaker.alloc.compat.Utils.parseDec;
26
import static uk.ac.manchester.spinnaker.alloc.compat.Utils.state;
27
import static uk.ac.manchester.spinnaker.alloc.compat.Utils.timestamp;
28
import static uk.ac.manchester.spinnaker.alloc.model.PowerState.ON;
29
import static uk.ac.manchester.spinnaker.machine.MachineDefaults.HALF_SIZE;
30
import static uk.ac.manchester.spinnaker.machine.MachineDefaults.SIZE_X_OF_ONE_BOARD;
31
import static uk.ac.manchester.spinnaker.machine.MachineDefaults.SIZE_Y_OF_ONE_BOARD;
32
import static uk.ac.manchester.spinnaker.machine.MachineDefaults.TRIAD_HEIGHT;
33
import static uk.ac.manchester.spinnaker.machine.MachineDefaults.TRIAD_WIDTH;
34
import static uk.ac.manchester.spinnaker.utils.CollectionUtils.collectToArray;
35
import static uk.ac.manchester.spinnaker.utils.UnitConstants.NSEC_PER_SEC;
36

37
import java.io.IOException;
38
import java.io.Reader;
39
import java.io.Writer;
40
import java.net.Socket;
41
import java.time.Duration;
42
import java.util.ArrayList;
43
import java.util.HashMap;
44
import java.util.List;
45
import java.util.Map;
46
import java.util.Objects;
47
import java.util.concurrent.Future;
48

49
import javax.annotation.PostConstruct;
50

51
import org.slf4j.Logger;
52
import org.springframework.beans.factory.annotation.Autowired;
53
import org.springframework.dao.DataAccessException;
54
import org.springframework.stereotype.Component;
55

56
import uk.ac.manchester.spinnaker.alloc.SpallocProperties;
57
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs;
58
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI;
59
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.BoardLocation;
60
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateDimensions;
61
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateNumBoards;
62
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.Job;
63
import uk.ac.manchester.spinnaker.alloc.compat.Utils.Notifier;
64
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
65
import uk.ac.manchester.spinnaker.alloc.model.JobListEntryRecord;
66
import uk.ac.manchester.spinnaker.alloc.model.PowerState;
67
import uk.ac.manchester.spinnaker.alloc.model.Prototype;
68
import uk.ac.manchester.spinnaker.alloc.security.Permit;
69
import uk.ac.manchester.spinnaker.machine.ChipLocation;
70
import uk.ac.manchester.spinnaker.machine.CoreLocation;
71
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
72
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
73
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
74
import uk.ac.manchester.spinnaker.spalloc.messages.BoardCoordinates;
75
import uk.ac.manchester.spinnaker.spalloc.messages.BoardPhysicalCoordinates;
76
import uk.ac.manchester.spinnaker.spalloc.messages.Connection;
77
import uk.ac.manchester.spinnaker.spalloc.messages.JobDescription;
78
import uk.ac.manchester.spinnaker.spalloc.messages.JobMachineInfo;
79
import uk.ac.manchester.spinnaker.spalloc.messages.JobState;
80
import uk.ac.manchester.spinnaker.spalloc.messages.Machine;
81
import uk.ac.manchester.spinnaker.spalloc.messages.WhereIs;
82

83
/**
84
 * Concrete implementation of a task handling a single client connection. This
85
 * is a <em>prototype bean</em>; Spring will instantiate it whenever needed.
86
 * It's consumed by {@link V1CompatService}, which uses it to implement the
87
 * handler for a particular connection.
88
 *
89
 * @author Donal Fellows
90
 */
91
@Component
92
@Prototype
93
class V1TaskImpl extends V1CompatTask {
94

95
        /**
96
         * We are compatible with spalloc-server release version 5.0.0.
97
         */
98
        public static final String VERSION = "5.0.0";
99

100
        private static final int LOTS = 10000;
101

102
        private static final Logger log = getLogger(V1CompatService.class);
3✔
103

104
        private final Map<Integer, Future<Void>> jobNotifiers = new HashMap<>();
3✔
105

106
        private final Map<String, Future<Void>> machNotifiers = new HashMap<>();
3✔
107

108
        /** The overall service properties. */
109
        @Autowired
110
        private SpallocProperties mainProps;
111

112
        /** The core spalloc service. */
113
        @Autowired
114
        private SpallocAPI spalloc;
115

116
        @Autowired
117
        private CompatHelper helper;
118

119
        @Autowired
120
        private Epochs epochs;
121

122
        /** Encoded form of our special permissions token. */
123
        private Permit permit;
124

125
        /** Name of the group for accounting purposes. */
126
        private String groupName;
127

128
        V1TaskImpl(V1CompatService srv, Socket sock) throws IOException {
129
                super(srv, sock);
×
130
        }
×
131

132
        V1TaskImpl(V1CompatService srv, Reader in, Writer out) {
133
                super(srv, in, out);
3✔
134
        }
3✔
135

136
        @PostConstruct
137
        void initUser() {
138
                var props = mainProps.getCompat();
3✔
139
                permit = new Permit(props.getServiceUser());
3✔
140
                groupName = props.getServiceGroup();
3✔
141
        }
3✔
142

143
        @Override
144
        protected final void closeNotifiers() {
145
                for (var n : jobNotifiers.values()) {
3✔
146
                        n.cancel(true);
×
147
                }
×
148
                for (var n : machNotifiers.values()) {
3✔
149
                        n.cancel(true);
×
150
                }
×
151
        }
3✔
152

153
        @Override
154
        protected final String version() {
155
                return VERSION;
3✔
156
        }
157

158
        @Override
159
        protected final JobMachineInfo getJobMachineInfo(int jobId)
160
                        throws TaskException {
161
                var job = getJob(jobId);
3✔
162
                job.access(host());
3✔
163
                var machine = job.getMachine()
3✔
164
                                .orElseThrow(() -> new TaskException("boards not allocated"));
3✔
165
                int w, h;
166
                if (machine.getDepth() == 1) {
3✔
167
                        // Special case: single board alloc
168
                        w = SIZE_X_OF_ONE_BOARD;
3✔
169
                        h = SIZE_Y_OF_ONE_BOARD;
3✔
170
                } else {
171
                        // Everything else is in terms of triads; careful of wraparounds
172
                        var full = machine.getMachine();
×
173
                        int linearTriadSize = job.getWidth().orElse(0);
×
174
                        w = linearTriadSize * TRIAD_WIDTH;
×
175
                        if (linearTriadSize != full.getWidth()
×
176
                                        || !full.isHorizonallyWrapped()) {
×
177
                                w += HALF_SIZE;
×
178
                        }
179
                        linearTriadSize = job.getHeight().orElse(0);
×
180
                        h = linearTriadSize * TRIAD_HEIGHT;
×
181
                        if (linearTriadSize != full.getHeight()
×
182
                                        || !full.isVerticallyWrapped()) {
×
183
                                h += HALF_SIZE;
×
184
                        }
185
                }
186
                return new JobMachineInfo(w, h, machine.getConnections().stream()
3✔
187
                                .map(ci -> new Connection(ci.getChip(), ci.getHostname()))
3✔
188
                                .collect(toList()), machine.getMachine().getName(),
3✔
189
                                machine.getBoards());
3✔
190
        }
191

192
        @Override
193
        protected final JobState getJobState(int jobId) throws TaskException {
194
                var job = getJob(jobId);
3✔
195
                job.access(host());
3✔
196
                var js = new JobState.Builder();
3✔
197
                js.setKeepalive(timestamp(job.getKeepaliveTimestamp()));
3✔
198
                js.setKeepalivehost(job.getKeepaliveHost().orElse(""));
3✔
199
                js.setPower(job.getMachine().map(m -> {
3✔
200
                        try {
201
                                return m.getPower() == ON;
3✔
202
                        } catch (DataAccessException e) {
×
203
                                log.warn("problem getting job power state", e);
×
204
                                return false;
×
205
                        }
206
                }).orElse(false));
3✔
207
                js.setReason(job.getReason().orElse(""));
3✔
208
                js.setStartTime(timestamp(job.getStartTime()));
3✔
209
                js.setState(state(job.getState()));
3✔
210
                return js.build();
3✔
211
        }
212

213
        @Override
214
        protected final void jobKeepalive(int jobId) throws TaskException {
215
                getJob(jobId).access(host());
3✔
216
        }
3✔
217

218
        /**
219
         * Parse a value to get a keepalive duration.
220
         *
221
         * @param keepalive
222
         *            The number to parse. May be {@code null} to get a default.
223
         * @return The duration. Never {@code null}.
224
         */
225
        private Duration parseKeepalive(Number keepalive) {
226
                if (isNull(keepalive)) {
3✔
227
                        return mainProps.getCompat().getDefaultKeepalive();
3✔
228
                }
229
                var d = Duration.ofSeconds(keepalive.longValue());
×
230
                if (!(keepalive instanceof Double || keepalive instanceof Float)) {
×
231
                        return d;
×
232
                }
233
                double fractionalPart = keepalive.doubleValue() - keepalive.longValue();
×
234
                return d.plusNanos((long) (fractionalPart * NSEC_PER_SEC));
×
235
        }
236

237
        /**
238
         * Parse a value to get a list of machine tags to match.
239
         *
240
         * @param src
241
         *            The value to parse.
242
         * @param mayForceDefault
243
         *            Whether we want to force a default value into the tags.
244
         * @return The list of tags. Never {@code null}.
245
         */
246
        private static List<String> tags(Object src, boolean mayForceDefault) {
247
                var vals = new ArrayList<String>();
3✔
248
                if (src instanceof List) {
3✔
249
                        for (var o : (List<?>) src) {
×
250
                                vals.add(Objects.toString(o));
×
251
                        }
×
252
                } else if (src instanceof String) {
3✔
253
                        vals.add((String) src);
×
254
                }
255
                if (vals.isEmpty() && mayForceDefault) {
3✔
256
                        return List.of("default");
×
257
                }
258
                return vals;
3✔
259
        }
260

261
        @Override
262
        protected final int createJobNumBoards(int numBoards,
263
                        Map<String, Object> kwargs, byte[] cmd) throws TaskException {
264
                var maxDead = parseDec(kwargs.get("max_dead_boards"));
3✔
265
                return createJob(new CreateNumBoards(numBoards, maxDead), kwargs, cmd);
3✔
266
        }
267

268
        @Override
269
        protected final int createJobRectangle(int width, int height,
270
                        Map<String, Object> kwargs, byte[] cmd) throws TaskException {
271
                var maxDead = parseDec(kwargs.get("max_dead_boards"));
3✔
272
                return createJob(new CreateDimensions(width, height, maxDead), kwargs,
3✔
273
                                cmd);
274
        }
275

276
        @Override
277
        protected final int createJobSpecificBoard(TriadCoords coords,
278
                        Map<String, Object> kwargs, byte[] cmd) throws TaskException {
279
                return createJob(triad(coords.x, coords.y, coords.z), kwargs, cmd);
3✔
280
        }
281

282
        private static String getOwner(Map<String, Object> kwargs)
283
                        throws TaskException {
284
                var owner = Objects.toString(kwargs.get("owner"), "").strip();
3✔
285
                if (owner.isBlank()) {
3✔
286
                        throw new TaskException(
×
287
                                        "invalid owner identifier; must be non-empty");
288
                }
289
                if (!isAsciiPrintable(owner)) {
3✔
290
                        throw new TaskException(
×
291
                                        "invalid owner identifier; must be printable ASCII");
292
                }
293
                return owner;
3✔
294
        }
295

296
        private Integer createJob(SpallocAPI.CreateDescriptor create,
297
                        Map<String, Object> kwargs, byte[] cmd) throws TaskException {
298
                var owner = getOwner(kwargs);
3✔
299
                var keepalive = parseKeepalive((Number) kwargs.get("keepalive"));
3✔
300
                var machineName = (String) kwargs.get("machine");
3✔
301
                var ts = tags(kwargs.get("tags"), isNull(machineName));
3✔
302
                var job = permit.authorize(() -> spalloc.createJobInGroup(
3✔
303
                                permit.name, groupName, create, machineName, ts, keepalive,
304
                                cmd));
305
                log.info("made compatibility-mode job {} on behalf of claimed user {}",
3✔
306
                                job.getId(), owner);
3✔
307
                return job.getId();
3✔
308
        }
309

310
        @Override
311
        protected final void destroyJob(int jobId, String reason)
312
                        throws TaskException {
313
                getJob(jobId).destroy(reason);
3✔
314
        }
3✔
315

316
        @Override
317
        protected final BoardCoordinates getBoardAtPhysicalPosition(
318
                        String machineName, int cabinet, int frame, int board)
319
                        throws TaskException {
320
                return getMachine(machineName)
3✔
321
                                .getBoardByPhysicalCoords(
3✔
322
                                                new PhysicalCoords(cabinet, frame, board))
323
                                .orElseThrow(() -> new TaskException("no such board"))
3✔
324
                                .getLogical();
3✔
325
        }
326

327
        @Override
328
        protected final BoardPhysicalCoordinates getBoardAtLogicalPosition(
329
                        String machineName, int x, int y, int z) throws TaskException {
330
                return getMachine(machineName)
3✔
331
                                .getBoardByLogicalCoords(new TriadCoords(x, y, z))
3✔
332
                                .orElseThrow(() -> new TaskException("no such board"))
3✔
333
                                .getPhysical();
3✔
334
        }
335

336
        @Component
337
        @SuppressWarnings("checkstyle:finalclass")
338
        private static class CompatHelper extends DatabaseAwareBean {
339
                /** The core spalloc service. */
340
                @Autowired
341
                private SpallocAPI spalloc;
342

343
                private JobDescription[] listJobs(V1TaskImpl task) {
344
                        // Messy; hits the database many times
345
                        return spalloc.listJobs(task.permit).stream()
3✔
346
                                        .map(job -> buildJobDescription(task, job))
3✔
347
                                        .collect(collectToArray(JobDescription[]::new));
3✔
348
                }
349

350
                private static JobDescription buildJobDescription(V1TaskImpl task,
351
                                JobListEntryRecord job) {
352
                        var jd = new JobDescription.Builder();
3✔
353
                        jd.setJobID(job.getId());
3✔
354
                        jd.setOwner(job.getOwner().orElse("<Hidden>"));
3✔
355
                        jd.setKeepAlive(job.getKeepaliveInterval().getSeconds());
3✔
356
                        jd.setKeepAliveHost(job.getHost().orElse("<Hidden>"));
3✔
357
                        jd.setReason("");
3✔
358
                        jd.setStartTime(
3✔
359
                                        (double) job.getCreationTimestamp().getEpochSecond());
3✔
360
                        jd.setState(state(job.getState()));
3✔
361
                        job.getOriginalRequest().map(task::parseCommand).ifPresent(cmd -> {
3✔
362
                                // In order to get here, this must be safe
363
                                // Validation was when job was created
364
                                @SuppressWarnings({ "unchecked", "rawtypes" })
365
                                List<Integer> args = (List) cmd.getArgs();
3✔
366
                                jd.setArgs(args);
3✔
367
                                jd.setKwargs(cmd.getKwargs());
3✔
368
                                // Override shrouded owner from above
369
                                Object owner = cmd.getKwargs().get("owner");
3✔
370
                                if (owner != null) {
3✔
371
                                        jd.setOwner(owner.toString());
3✔
372
                                }
373
                        });
3✔
374
                        jd.setMachine(job.getMachineName());
3✔
375
                        jd.setPower(job.isPowered());
3✔
376
                        jd.setBoards(job.getBoards().stream().map(
3✔
377
                                        b -> new BoardCoordinates(b.getX(), b.getY(), b.getZ()))
×
378
                                        .collect(toList()));
3✔
379
                        return jd.build();
3✔
380
                }
381

382
                private Machine[] listMachines() {
383
                        // Messy; hits the database many times
384
                        return executeRead(c -> spalloc.getMachines(false).values().stream()
3✔
385
                                        .map(CompatHelper::buildMachineDescription)
3✔
386
                                        .collect(collectToArray(Machine[]::new)));
3✔
387
                }
388

389
                private static Machine buildMachineDescription(SpallocAPI.Machine m) {
390
                        return new Machine(m.getName(), List.copyOf(m.getTags()),
3✔
391
                                        m.getWidth(), m.getHeight(),
3✔
392
                                        m.getDeadBoards().stream().map(Utils::board)
3✔
393
                                                        .collect(toList()),
3✔
394
                                        m.getDownLinks().stream().flatMap(Utils::boardLinks)
3✔
395
                                                        .collect(toList()));
3✔
396
                }
397
        }
398

399
        @Override
400
        protected final JobDescription[] listJobs() {
401
                return permit.authorize(() -> helper.listJobs(this));
3✔
402
        }
403

404
        @Override
405
        protected final Machine[] listMachines() {
406
                return permit.authorize(helper::listMachines);
3✔
407
        }
408

409
        @Override
410
        protected final void notifyJob(Integer jobId, boolean wantNotify)
411
                        throws TaskException {
412
                if (nonNull(jobId)) {
3✔
413
                        var job = getJob(jobId);
3✔
414
                        job.access(host());
3✔
415
                        manageNotifier(jobNotifiers, jobId, wantNotify, () -> {
3✔
416
                                log.debug("Looking for changes to {}", job.getId());
3✔
417
                                if (job.waitForChange(
3✔
418
                                                mainProps.getCompat().getNotifyWaitTime())) {
3✔
419
                                        log.debug("Job {} changed!", jobId);
×
420
                                        writeJobNotification(List.of(jobId));
×
421
                                }
422
                                log.debug("Stopped looking for changes to {}", jobId);
3✔
423
                        });
3✔
424
                } else {
3✔
425
                        manageNotifier(jobNotifiers, jobId, wantNotify, () -> {
3✔
426
                                var actual = permit.authorize(() -> {
3✔
427
                                        log.debug("Looking for changes to all jobs");
3✔
428
                                        return spalloc.getJobs(false, LOTS, 0).getChanged(
3✔
429
                                                        mainProps.getCompat().getNotifyWaitTime());
3✔
430
                                });
431
                                if (actual.size() > 0) {
3✔
432
                                        log.debug("Jobs {} changed!", actual);
×
433
                                        writeJobNotification(
×
434
                                                        actual.stream().collect(toList()));
×
435
                                }
436
                                log.debug("Stopped looking for changes to all jobs");
3✔
437
                        });
3✔
438
                }
439
        }
3✔
440

441
        @Override
442
        protected final void notifyMachine(String machineName, boolean wantNotify)
443
                        throws TaskException {
444
                if (nonNull(machineName)) {
3✔
445
                        var machine = getMachine(machineName);
3✔
446
                        manageNotifier(machNotifiers, machineName, wantNotify, () -> {
3✔
447
                                if (machine.waitForChange(
3✔
448
                                                mainProps.getCompat().getNotifyWaitTime())) {
3✔
449
                                        writeMachineNotification(List.of(machineName));
×
450
                                }
451
                        });
3✔
452
                } else {
3✔
453
                        manageNotifier(machNotifiers, machineName, wantNotify, () -> {
3✔
454
                                List<String> actual = permit.authorize(() -> {
3✔
455
                                        var machines = spalloc.getMachines(false);
3✔
456
                                        if (machines.isEmpty()) {
3✔
457
                                                // No machines, so don't wait
458
                                                return List.of();
×
459
                                        }
460
                                        var invMap = machines.values().stream()
3✔
461
                                                        .collect(toMap(SpallocAPI.Machine::getId,
3✔
462
                                                                        SpallocAPI.Machine::getName));
463
                                        var mIds = machines.values().stream().map(m -> m.getId())
3✔
464
                                                        .collect(toList());
3✔
465
                                        var epoch = epochs.getMachinesEpoch(mIds);
3✔
466
                                        try {
467
                                                var changed = epoch.getChanged(
3✔
468
                                                                mainProps.getCompat().getNotifyWaitTime());
3✔
469
                                                return changed.stream().map(id -> invMap.get(id))
3✔
470
                                                                .collect(toList());
3✔
471
                                        } catch (InterruptedException e) {
3✔
472
                                                return List.of();
3✔
473
                                        }
474
                                });
475
                                if (actual.size() > 0) {
3✔
476
                                        writeMachineNotification(actual.stream().collect(toList()));
×
477
                                }
478
                        });
3✔
479
                }
480
        }
3✔
481

482
        @Override
483
        protected final void powerJobBoards(int jobId, PowerState switchOn)
484
                        throws TaskException {
485
                var job = getJob(jobId);
3✔
486
                job.access(host());
3✔
487
                job.getMachine().orElseThrow(
3✔
488
                                () -> new TaskException("no boards currently allocated"))
×
489
                                .setPower(switchOn);
3✔
490
        }
3✔
491

492
        @Override
493
        protected void reportProblem(String address, Integer x, Integer y,
494
                        Integer p, String description) {
495
                HasChipLocation locus;
496
                if (nonNull(x) && nonNull(y)) {
×
497
                        if (nonNull(p)) {
×
498
                                locus = new CoreLocation(x, y, p);
×
499
                        } else {
500
                                locus = new ChipLocation(x, y);
×
501
                        }
502
                } else {
503
                        locus = null;
×
504
                }
505
                permit.authorize(() -> {
×
506
                        spalloc.reportProblem(address, locus, description, permit);
×
507
                        return this; // Ignored value
×
508
                });
509
        }
×
510

511
        private static WhereIs makeWhereIs(BoardLocation bl) {
512
                var wi = new WhereIs.Builder();
3✔
513
                wi.setMachine(bl.getMachine());
3✔
514
                wi.setLogical(bl.getLogical());
3✔
515
                wi.setPhysical(bl.getPhysical());
3✔
516
                wi.setChip(bl.getChip());
3✔
517
                wi.setBoardChip(bl.getBoardChip());
3✔
518
                var j = bl.getJob();
3✔
519
                if (nonNull(j)) {
3✔
520
                        wi.setJobId(j.getId());
3✔
521
                        wi.setJobChip(bl.getChipRelativeTo(j.getRootChip().orElseThrow()));
3✔
522
                }
523
                return wi.build();
3✔
524
        }
525

526
        @Override
527
        protected final WhereIs whereIsJobChip(int jobId, int x, int y)
528
                        throws TaskException {
529
                var job = getJob(jobId);
3✔
530
                job.access(host());
3✔
531
                return job.whereIs(x, y).map(V1TaskImpl::makeWhereIs).orElseThrow(
3✔
532
                                () -> new TaskException("no boards currently allocated"));
×
533
        }
534

535
        @Override
536
        protected final WhereIs whereIsMachineChip(String machineName, int x, int y)
537
                        throws TaskException {
538
                return getMachine(machineName).getBoardByChip(new ChipLocation(x, y))
3✔
539
                                .map(V1TaskImpl::makeWhereIs)
3✔
540
                                .orElseThrow(() -> new TaskException("no such board"));
3✔
541
        }
542

543
        @Override
544
        protected final WhereIs whereIsMachineLogicalBoard(String machineName,
545
                        int x, int y, int z) throws TaskException {
546
                return getMachine(machineName)
3✔
547
                                .getBoardByLogicalCoords(new TriadCoords(x, y, z))
3✔
548
                                .map(V1TaskImpl::makeWhereIs)
3✔
549
                                .orElseThrow(() -> new TaskException("no such board"));
3✔
550
        }
551

552
        @Override
553
        protected final WhereIs whereIsMachinePhysicalBoard(String machineName,
554
                        int c, int f, int b) throws TaskException {
555
                return getMachine(machineName)
3✔
556
                                .getBoardByPhysicalCoords(new PhysicalCoords(c, f, b))
3✔
557
                                .map(V1TaskImpl::makeWhereIs)
3✔
558
                                .orElseThrow(() -> new TaskException("no such board"));
3✔
559
        }
560

561
        // instance-aware utilities
562

563
        private Job getJob(int jobId) throws TaskException {
564
                return permit.authorize(() -> spalloc.getJob(permit, jobId))
3✔
565
                                .orElseThrow(() -> new TaskException("no such job"));
3✔
566
        }
567

568
        private SpallocAPI.Machine getMachine(String machineName)
569
                        throws TaskException {
570
                return permit.authorize(() -> spalloc.getMachine(machineName, false))
3✔
571
                                .orElseThrow(() -> new TaskException("no such machine"));
3✔
572
        }
573

574
        private <T> void manageNotifier(Map<T, Future<Void>> notifiers, T key,
575
                        boolean wantNotify, Notifier notifier) {
576
                if (wantNotify) {
3✔
577
                        if (!notifiers.containsKey(key)) {
3✔
578
                                notifiers.put(key,
3✔
579
                                                getExecutor().submit(Notifier.toCallable(notifier)));
3✔
580
                        }
581
                } else {
582
                        var n = notifiers.remove(key);
3✔
583
                        if (nonNull(n)) {
3✔
584
                                n.cancel(true);
3✔
585
                        }
586
                }
587
        }
3✔
588
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc