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

SpiNNakerManchester / JavaSpiNNaker / 6998

01 Sep 2025 06:27AM UTC coverage: 36.303% (+0.03%) from 36.276%
6998

push

github

rowleya
OIDC users can use private collab

1912 of 5900 branches covered (32.41%)

Branch coverage included in aggregate %.

7 of 8 new or added lines in 1 file covered. (87.5%)

6 existing lines in 1 file now uncovered.

8975 of 24089 relevant lines covered (37.26%)

0.74 hits per line

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

65.82
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/allocator/Spalloc.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.allocator;
17

18
import static java.lang.Math.max;
19
import static java.lang.String.format;
20
import static java.lang.Thread.currentThread;
21
import static java.util.Objects.isNull;
22
import static java.util.Objects.nonNull;
23
import static java.util.stream.Collectors.toList;
24
import static java.util.stream.Collectors.toSet;
25
import static org.slf4j.LoggerFactory.getLogger;
26
import static uk.ac.manchester.spinnaker.alloc.Constants.TRIAD_CHIP_SIZE;
27
import static uk.ac.manchester.spinnaker.alloc.Constants.TRIAD_DEPTH;
28
import static uk.ac.manchester.spinnaker.alloc.db.Row.int64;
29
import static uk.ac.manchester.spinnaker.alloc.db.Row.integer;
30
import static uk.ac.manchester.spinnaker.alloc.db.Row.string;
31
import static uk.ac.manchester.spinnaker.alloc.db.Row.chip;
32
import static uk.ac.manchester.spinnaker.alloc.model.JobState.READY;
33
import static uk.ac.manchester.spinnaker.alloc.model.PowerState.OFF;
34
import static uk.ac.manchester.spinnaker.alloc.model.PowerState.ON;
35
import static uk.ac.manchester.spinnaker.alloc.security.LocalAuthProviderImpl.PRIVATE_COLLAB_PREFIX;
36
import static uk.ac.manchester.spinnaker.alloc.security.SecurityConfig.MAY_SEE_JOB_DETAILS;
37
import static uk.ac.manchester.spinnaker.utils.CollectionUtils.copy;
38
import static uk.ac.manchester.spinnaker.utils.OptionalUtils.apply;
39

40
import java.io.IOException;
41
import java.net.InetAddress;
42
import java.time.Duration;
43
import java.time.Instant;
44
import java.util.ArrayList;
45
import java.util.Collection;
46
import java.util.Formatter;
47
import java.util.HashMap;
48
import java.util.List;
49
import java.util.Locale;
50
import java.util.Map;
51
import java.util.Optional;
52
import java.util.Set;
53

54
import org.slf4j.Logger;
55
import org.springframework.beans.factory.annotation.Autowired;
56
import org.springframework.security.access.prepost.PostFilter;
57
import org.springframework.stereotype.Service;
58

59
import com.fasterxml.jackson.annotation.JsonIgnore;
60
import com.google.errorprone.annotations.FormatMethod;
61
import com.google.errorprone.annotations.concurrent.GuardedBy;
62

63
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.AllocatorProperties;
64
import uk.ac.manchester.spinnaker.alloc.admin.ReportMailSender;
65
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs.Epoch;
66
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
67
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Query;
68
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Update;
69
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
70
import uk.ac.manchester.spinnaker.alloc.db.Row;
71
import uk.ac.manchester.spinnaker.alloc.model.BoardCoords;
72
import uk.ac.manchester.spinnaker.alloc.model.ConnectionInfo;
73
import uk.ac.manchester.spinnaker.alloc.model.Direction;
74
import uk.ac.manchester.spinnaker.alloc.model.DownLink;
75
import uk.ac.manchester.spinnaker.alloc.model.JobDescription;
76
import uk.ac.manchester.spinnaker.alloc.model.JobListEntryRecord;
77
import uk.ac.manchester.spinnaker.alloc.model.JobState;
78
import uk.ac.manchester.spinnaker.alloc.model.MachineDescription;
79
import uk.ac.manchester.spinnaker.alloc.model.MachineDescription.JobInfo;
80
import uk.ac.manchester.spinnaker.alloc.model.MachineListEntryRecord;
81
import uk.ac.manchester.spinnaker.alloc.model.PowerState;
82
import uk.ac.manchester.spinnaker.alloc.proxy.ProxyCore;
83
import uk.ac.manchester.spinnaker.alloc.security.Permit;
84
import uk.ac.manchester.spinnaker.alloc.web.IssueReportRequest;
85
import uk.ac.manchester.spinnaker.alloc.web.IssueReportRequest.ReportedBoard;
86
import uk.ac.manchester.spinnaker.connections.SCPConnection;
87
import uk.ac.manchester.spinnaker.machine.ChipLocation;
88
import uk.ac.manchester.spinnaker.machine.CoreLocation;
89
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
90
import uk.ac.manchester.spinnaker.machine.HasCoreLocation;
91
import uk.ac.manchester.spinnaker.machine.MachineVersion;
92
import uk.ac.manchester.spinnaker.machine.board.BMPCoords;
93
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
94
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
95
import uk.ac.manchester.spinnaker.machine.tags.IPTag;
96
import uk.ac.manchester.spinnaker.protocols.FastDataIn;
97
import uk.ac.manchester.spinnaker.protocols.download.Downloader;
98
import uk.ac.manchester.spinnaker.spalloc.messages.BoardCoordinates;
99
import uk.ac.manchester.spinnaker.spalloc.messages.BoardPhysicalCoordinates;
100
import uk.ac.manchester.spinnaker.transceiver.ProcessException;
101
import uk.ac.manchester.spinnaker.transceiver.SpinnmanException;
102
import uk.ac.manchester.spinnaker.transceiver.Transceiver;
103
import uk.ac.manchester.spinnaker.transceiver.TransceiverInterface;
104

105
/**
106
 * The core implementation of the Spalloc service.
107
 *
108
 * @author Donal Fellows
109
 */
110
@Service
111
public class Spalloc extends DatabaseAwareBean implements SpallocAPI {
2✔
112
        private static final String NO_BOARD_MSG =
113
                        "request does not identify an existing board "
114
                                        + "or uses a prohibited coordinate";
115

116
        private static final Logger log = getLogger(Spalloc.class);
2✔
117

118
        @Autowired
119
        private PowerController powerController;
120

121
        @Autowired
122
        private Epochs epochs;
123

124
        @Autowired
125
        private QuotaManager quotaManager;
126

127
        @Autowired
128
        private ReportMailSender emailSender;
129

130
        @Autowired
131
        private AllocatorProperties props;
132

133
        @Autowired
134
        private JobObjectRememberer rememberer;
135

136
        @Autowired
137
        private AllocatorTask allocator;
138

139
        @GuardedBy("this")
2✔
140
        private transient Map<String, List<BoardCoords>> downBoardsCache =
141
                        new HashMap<>();
142

143
        @GuardedBy("this")
2✔
144
        private transient Map<String, List<DownLink>> downLinksCache =
145
                        new HashMap<>();
146

147
        private boolean emergencyStop = false;
2✔
148

149
        @Override
150
        public Map<String, Machine> getMachines(boolean allowOutOfService) {
151
                return executeRead(c -> getMachines(c, allowOutOfService));
2✔
152
        }
153

154
        private Map<String, Machine> getMachines(Connection conn,
155
                        boolean allowOutOfService) {
156
                try (var listMachines = conn.query(GET_ALL_MACHINES)) {
2✔
157
                        return Row.stream(listMachines.call(
2✔
158
                                        row -> new MachineImpl(conn, row), allowOutOfService))
2✔
159
                                        .toMap(Machine::getName, (m) -> m);
2✔
160
                }
161
        }
162

163
        private final class ListMachinesSQL extends AbstractSQL {
2✔
164
                private final Query listMachines = conn.query(GET_ALL_MACHINES);
2✔
165

166
                private final Query countMachineThings =
2✔
167
                                conn.query(COUNT_MACHINE_THINGS);
2✔
168

169
                private final Query getTags = conn.query(GET_TAGS);
2✔
170

171
                @Override
172
                public void close() {
173
                        listMachines.close();
2✔
174
                        countMachineThings.close();
2✔
175
                        getTags.close();
2✔
176
                        super.close();
2✔
177
                }
2✔
178

179
                private MachineListEntryRecord makeMachineListEntryRecord(Row row) {
180
                        int id = row.getInt("machine_id");
2✔
181
                        var rec = new MachineListEntryRecord();
2✔
182
                        rec.setId(id);
2✔
183
                        rec.setName(row.getString("machine_name"));
2✔
184
                        if (!countMachineThings.call1((m) -> {
2✔
185
                                rec.setNumBoards(m.getInt("board_count"));
2✔
186
                                rec.setNumInUse(m.getInt("in_use"));
2✔
187
                                rec.setNumJobs(m.getInt("num_jobs"));
2✔
188
                                rec.setTags(getTags.call(string("tag"), id));
2✔
189
                                // We have to return something but don't read the result
190
                                return true;
2✔
191
                        }, id).isPresent()) {
2!
192
                                throw new AssertionError("No result from count machine things");
×
193
                        }
194
                        return rec;
2✔
195
                }
196
        }
197

198
        @Override
199
        public List<MachineListEntryRecord>
200
                        listMachines(boolean allowOutOfService) {
201
                try (var sql = new ListMachinesSQL()) {
2✔
202
                        return sql.transactionRead(
2✔
203
                                        () -> sql.listMachines.call(
2✔
204
                                                        sql::makeMachineListEntryRecord,
2✔
205
                                                        allowOutOfService));
2✔
206
                }
207
        }
208

209
        @Override
210
        public Optional<Machine> getMachine(String name,
211
                        boolean allowOutOfService) {
212
                return executeRead(
2✔
213
                                conn -> getMachine(name, allowOutOfService, conn).map(m -> m));
2✔
214
        }
215

216
        private Optional<MachineImpl> getMachine(int id, boolean allowOutOfService,
217
                        Connection conn) {
218
                try (var idMachine = conn.query(GET_MACHINE_BY_ID)) {
2✔
219
                        return idMachine.call1(row -> new MachineImpl(conn, row),
2✔
220
                                        id, allowOutOfService);
2✔
221
                }
222
        }
223

224
        private Optional<MachineImpl> getMachine(String name,
225
                        boolean allowOutOfService, Connection conn) {
226
                try (var namedMachine = conn.query(GET_NAMED_MACHINE)) {
2✔
227
                        return namedMachine.call1(row -> new MachineImpl(conn, row),
2✔
228
                                        name, allowOutOfService);
2✔
229
                }
230
        }
231

232
        private final class DescribeMachineSQL extends AbstractSQL {
2✔
233
                private final Query namedMachine = conn.query(GET_NAMED_MACHINE);
2✔
234

235
                private final Query countMachineThings = conn.query(
2✔
236
                                COUNT_MACHINE_THINGS);
237

238
                private final Query getTags = conn.query(GET_TAGS);
2✔
239

240
                private final Query getJobs = conn.query(GET_MACHINE_JOBS);
2✔
241

242
                private final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
2✔
243

244
                private final Query getLive = conn.query(GET_LIVE_BOARDS);
2✔
245

246
                private final Query getDead = conn.query(GET_DEAD_BOARDS);
2✔
247

248
                private final Query getQuota = conn.query(GET_USER_QUOTA);
2✔
249

250
                @Override
251
                public void close() {
252
                        namedMachine.close();
2✔
253
                        countMachineThings.close();
2✔
254
                        getTags.close();
2✔
255
                        getJobs.close();
2✔
256
                        getCoords.close();
2✔
257
                        getLive.close();
2✔
258
                        getDead.close();
2✔
259
                        getQuota.close();
2✔
260
                        super.close();
2✔
261
                }
2✔
262
        }
263

264
        @Override
265
        public Optional<MachineDescription> getMachineInfo(String machine,
266
                        boolean allowOutOfService, Permit permit) {
267
                try (var sql = new DescribeMachineSQL()) {
2✔
268
                        return sql.transactionRead(() -> apply(
2✔
269
                                        sql.namedMachine.call1(Spalloc::getBasicMachineInfo,
2✔
270
                                                        machine, allowOutOfService),
2✔
271
                                        md -> sql.countMachineThings.call1(integer("in_use"),
2✔
272
                                                        md.getId()).ifPresent(md::setNumInUse),
2✔
273
                                        md -> md.setTags(
2✔
274
                                                        sql.getTags.call(string("tag"), md.getId())),
2✔
275
                                        md -> md.setJobs(sql.getJobs.call(
2✔
276
                                                        row -> getMachineJobInfo(permit, sql.getCoords,
2✔
277
                                                                        row),
278
                                                        md.getId())),
2✔
279
                                        md -> md.setLive(sql.getLive.call(
2✔
280
                                                        row -> new BoardCoords(row, !permit.admin),
×
281
                                                        md.getId())),
2✔
282
                                        md -> md.setDead(sql.getDead.call(
2✔
283
                                                        row -> new BoardCoords(row, !permit.admin),
×
284
                                                        md.getId())),
2✔
285
                                        md -> sql.getQuota.call1(int64("quota_total"), permit.name)
2✔
286
                                                        .ifPresent(md::setQuota)));
2✔
287
                }
288
        }
289

290
        private static MachineDescription getBasicMachineInfo(Row row) {
291
                var md = new MachineDescription();
2✔
292
                md.setId(row.getInt("machine_id"));
2✔
293
                md.setName(row.getString("machine_name"));
2✔
294
                md.setWidth(row.getInt("width"));
2✔
295
                md.setHeight(row.getInt("height"));
2✔
296
                return md;
2✔
297
        }
298

299
        private static JobInfo getMachineJobInfo(Permit permit, Query getCoords,
300
                        Row row) {
301
                int jobId = row.getInt("job_id");
2✔
302
                var mayUnveil = permit.unveilFor(row.getString("owner_name"));
2✔
303
                var owner = mayUnveil ? row.getString("owner_name") : null;
2!
304

305
                var ji = new JobInfo();
2✔
306
                ji.setId(jobId);
2✔
307
                ji.setOwner(owner);
2✔
308
                ji.setBoards(
2✔
309
                                getCoords.call(r -> new BoardCoords(r, !mayUnveil), jobId));
2!
310
                return ji;
2✔
311
        }
312

313
        @Override
314
        public Jobs getJobs(boolean deleted, int limit, int start) {
315
                return executeRead(conn -> {
2✔
316
                        if (deleted) {
2✔
317
                                try (var jobs = conn.query(GET_JOB_IDS)) {
2✔
318
                                        return new JobCollection(
2✔
319
                                                        jobs.call(this::makeJob, limit, start));
2✔
320
                                }
321
                        } else {
322
                                try (var jobs = conn.query(GET_LIVE_JOB_IDS)) {
2✔
323
                                        return new JobCollection(
2✔
324
                                                        jobs.call(this::makeJob, limit, start));
2✔
325
                                }
326
                        }
327
                });
328
        }
329

330
        /**
331
         * Makes "partial" jobs; some fields are shrouded, modifications are
332
         * disabled.
333
         *
334
         * @param row
335
         *            The row to make the job from.
336
         */
337
        private Job makeJob(Row row) {
338
                int jobId = row.getInt("job_id");
2✔
339
                int machineId = row.getInt("machine_id");
2✔
340
                var jobState = row.getEnum("job_state", JobState.class);
2✔
341
                var keepalive = row.getInstant("keepalive_timestamp");
2✔
342
                return new JobImpl(jobId, machineId, jobState, keepalive);
2✔
343
        }
344

345
        @Override
346
        public List<JobListEntryRecord> listJobs(Permit permit) {
347
                return executeRead(conn -> {
2✔
348
                        try (var listLiveJobs = conn.query(LIST_LIVE_JOBS);
2✔
349
                                        var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
2✔
350
                                        var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
2✔
351
                                return listLiveJobs.call(row -> makeJobListEntryRecord(permit,
2✔
352
                                                countPoweredBoards, getCoords, row));
353
                        }
354
                });
355
        }
356

357
        private static JobListEntryRecord makeJobListEntryRecord(Permit permit,
358
                        Query countPoweredBoards, Query getCoords, Row row) {
359
                var rec = new JobListEntryRecord();
2✔
360
                int id = row.getInt("job_id");
2✔
361
                rec.setId(id);
2✔
362
                rec.setState(row.getEnum("job_state", JobState.class));
2✔
363
                var numBoards = row.getInteger("allocation_size");
2✔
364
                rec.setPowered(nonNull(numBoards) && numBoards.equals(countPoweredBoards
2!
365
                                .call1(integer("c"), id).orElseThrow()));
×
366
                rec.setMachineId(row.getInt("machine_id"));
2✔
367
                rec.setMachineName(row.getString("machine_name"));
2✔
368
                rec.setCreationTimestamp(row.getInstant("create_timestamp"));
2✔
369
                rec.setKeepaliveInterval(row.getDuration("keepalive_interval"));
2✔
370
                rec.setBoards(getCoords.call(r -> new BoardCoords(r, false), id));
2✔
371
                rec.setOriginalRequest(row.getBytes("original_request"));
2✔
372
                var owner = row.getString("user_name");
2✔
373
                if (permit.unveilFor(owner)) {
2!
374
                        rec.setOwner(owner);
2✔
375
                        rec.setHost(row.getString("keepalive_host"));
2✔
376
                }
377
                return rec;
2✔
378
        }
379

380
        @Override
381
        @PostFilter(MAY_SEE_JOB_DETAILS)
382
        public Optional<Job> getJob(Permit permit, int id) {
383
                return executeRead(conn -> getJob(id, conn).map(j -> (Job) j));
2✔
384
        }
385

386
        private Optional<JobImpl> getJob(int id, Connection conn) {
387
                try (var s = conn.query(GET_JOB)) {
2✔
388
                        return s.call1(row -> new JobImpl(conn, row), id);
2✔
389
                }
390
        }
391

392
        @Override
393
        @PostFilter(MAY_SEE_JOB_DETAILS)
394
        public Optional<JobDescription> getJobInfo(Permit permit, int id) {
395
                return executeRead(conn -> {
2✔
396
                        try (var s = conn.query(GET_JOB);
2✔
397
                                        var chipDimensions = conn.query(GET_JOB_CHIP_DIMENSIONS);
2✔
398
                                        var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
2✔
399
                                        var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
2✔
400
                                return s.call1(row -> jobDescription(id, row,
2✔
401
                                                chipDimensions, countPoweredBoards, getCoords), id);
2✔
402
                        }
403
                });
404
        }
405

406
        private static JobDescription jobDescription(int id, Row job,
407
                        Query chipDimensions, Query countPoweredBoards, Query getCoords) {
408
                /*
409
                 * We won't deliver this object to the front end unless they are allowed
410
                 * to see it in its entirety.
411
                 */
412
                var jd = new JobDescription();
2✔
413
                jd.setId(id);
2✔
414
                jd.setMachine(job.getString("machine_name"));
2✔
415
                jd.setState(job.getEnum("job_state", JobState.class));
2✔
416
                jd.setOwner(job.getString("owner"));
2✔
417
                jd.setOwnerHost(job.getString("keepalive_host"));
2✔
418
                jd.setStartTime(job.getInstant("create_timestamp"));
2✔
419
                jd.setKeepAlive(job.getDuration("keepalive_interval"));
2✔
420
                jd.setRequestBytes(job.getBytes("original_request"));
2✔
421
                chipDimensions.call1(cd -> {
2✔
422
                        jd.setWidth(cd.getInt("width"));
2✔
423
                        jd.setHeight(cd.getInt("height"));
2✔
424
                        // We have to return something but we ignore it anyway
425
                        return true;
2✔
426
                }, id);
2✔
427
                int poweredCount =
2✔
428
                                countPoweredBoards.call1(integer("c"), id).orElseThrow();
2✔
429
                jd.setBoards(getCoords.call(r -> new BoardCoords(r, false), id));
2✔
430
                jd.setPowered(jd.getBoards().size() == poweredCount);
2✔
431
                return jd;
2✔
432
        }
433

434
        @Override
435
        public Job createJobInGroup(String owner, String groupName,
436
                        CreateDescriptor descriptor, String machineName, List<String> tags,
437
                        Duration keepaliveInterval, byte[] req)
438
                                        throws IllegalArgumentException {
439
                if (emergencyStop) {
2!
440
                        throw new IllegalStateException(
×
441
                                        "The Server has been stopped due to an emergency.");
442
                }
443
                return execute(conn -> {
2✔
444
                        int user = getUser(conn, owner).orElseThrow(
2✔
445
                                        () -> new RuntimeException("no such user: " + owner));
×
446
                        int group = selectGroup(conn, owner, groupName);
2✔
447
                        if (!quotaManager.mayCreateJob(group)) {
2!
448
                                // No quota left
449
                                throw new IllegalArgumentException(
×
450
                                                "quota exceeded in group " + group);
451
                        }
452

453
                        var m = selectMachine(conn, descriptor, machineName, tags);
2✔
454
                        if (!m.isPresent()) {
2!
455
                                throw new IllegalArgumentException(
×
456
                                                "no machine available which matches allocation "
457
                                                + "request");
458
                        }
459
                        var machine = m.orElseThrow();
2✔
460

461
                        var id = insertJob(conn, machine, user, group, keepaliveInterval,
2✔
462
                                        req);
463
                        if (!id.isPresent()) {
2!
464
                                throw new RuntimeException("failed to create job");
×
465
                        }
466
                        int jobId = id.orElseThrow();
2✔
467

468
                        var scale = props.getPriorityScale();
2✔
469

470
                        if (machine.getArea() < descriptor.getArea()) {
2!
471
                                throw new IllegalArgumentException(
×
472
                                                "request cannot fit on machine");
473
                        }
474

475
                        // Ask the allocator engine to do the allocation
476
                        int numBoards = descriptor.visit(new CreateVisitor<Integer>() {
2✔
477
                                @Override
478
                                public Integer numBoards(CreateNumBoards nb) {
479
                                        try (var insertReq = conn.update(INSERT_REQ_N_BOARDS)) {
2✔
480
                                                insertReq.call(jobId, nb.numBoards, nb.maxDead,
2✔
481
                                                                (int) (nb.getArea() * scale.getSize()));
2✔
482
                                        }
483
                                        return nb.numBoards;
2✔
484
                                }
485

486
                                @Override
487
                                public Integer dimensions(CreateDimensions d) {
488
                                        try (var insertReq = conn.update(INSERT_REQ_SIZE)) {
2✔
489
                                                insertReq.call(jobId, d.width, d.height, d.maxDead,
2✔
490
                                                                (int) (d.getArea() * scale.getDimensions()));
2✔
491
                                        }
492
                                        return max(1, d.getArea() - d.maxDead);
2✔
493
                                }
494

495
                                /*
496
                                 * Request by area rooted at specific location; resolve to board
497
                                 * ID now, as that doesn't depend on whether the board is
498
                                 * currently in use.
499
                                 */
500
                                @Override
501
                                public Integer dimensionsAt(CreateDimensionsAt da) {
502
                                        try (var insertReq = conn.update(INSERT_REQ_SIZE_BOARD)) {
2✔
503
                                                insertReq.call(jobId,
2✔
504
                                                                locateBoard(conn, machine.name, da, true),
2✔
505
                                                                da.width, da.height, da.maxDead,
2✔
506
                                                                (int) scale.getSpecificBoard());
2✔
507
                                        }
508
                                        return max(1, da.getArea() - da.maxDead);
2✔
509
                                }
510

511
                                /*
512
                                 * Request by specific location; resolve to board ID now, as
513
                                 * that doesn't depend on whether the board is currently in
514
                                 * use.
515
                                 */
516
                                @Override
517
                                public Integer board(CreateBoard b) {
518
                                        try (var insertReq = conn.update(INSERT_REQ_BOARD)) {
2✔
519
                                                /*
520
                                                 * This doesn't pass along the max dead boards; only
521
                                                 * after one!
522
                                                 */
523
                                                insertReq.call(jobId,
2✔
524
                                                                locateBoard(conn, machine.name, b, false),
2✔
525
                                                                (int) scale.getSpecificBoard());
2✔
526
                                        }
527
                                        return 1;
2✔
528
                                }
529
                        });
530

531
                        // DB now changed; can report success
532
                        JobLifecycle.log.info(
2✔
533
                                        "created job {} on {} for {} asking for {} board(s)", jobId,
2✔
534
                                        machine.name, owner, numBoards);
2✔
535

536
                        allocator.scheduleAllocateNow();
2✔
537
                        return getJob(jobId, conn).map(ji -> (Job) ji).orElseThrow(
2✔
538
                                        () -> new RuntimeException("Error creating job!"));
×
539
                });
540
        }
541

542
        @Override
543
        public Job createJob(String owner, CreateDescriptor descriptor,
544
                        String machineName, List<String> tags, Duration keepaliveInterval,
545
                        byte[] originalRequest) {
546
                return execute(conn -> {
2✔
547
                        var isInternal = conn.query(GET_USER_DETAILS_BY_NAME).call1(
2✔
548
                                        (row) -> row.getInt("is_internal") == 1, owner)
2!
549
                                        .orElseThrow();
2✔
550

551
                        // OIDC users can use a private group
552
                        log.info("User {} is {}internal", owner, isInternal ? "" : "not ");
2!
553
                        if (!isInternal) {
2!
NEW
554
                                return createJobInCollabSession(
×
555
                                                owner, PRIVATE_COLLAB_PREFIX + owner, descriptor,
556
                                                machineName, tags, keepaliveInterval, originalRequest);
557
                        }
558
                        return createJobInGroup(
2✔
559
                                owner, getOnlyGroup(conn, owner), descriptor, machineName,
2✔
560
                                tags, keepaliveInterval, originalRequest);
561
                });
562
        }
563

564
        @Override
565
        public Job createJobInCollabSession(String owner,
566
                        String nmpiCollab, CreateDescriptor descriptor,
567
                        String machineName, List<String> tags, Duration keepaliveInterval,
568
                        byte[] originalRequest) {
569
                var session = quotaManager.createSession(nmpiCollab, owner);
×
570
                var quotaUnits = session.getResourceUsage().getUnits();
×
571

572
                // Use the Collab name as the group, as it should exist
573
                var job = execute(conn -> createJobInGroup(
×
574
                                owner, nmpiCollab, descriptor, machineName,
575
                                tags, keepaliveInterval, originalRequest));
576

577
                quotaManager.associateNMPISession(job.getId(), session.getId(),
×
578
                                quotaUnits);
579

580
                // Return the job created
581
                return job;
×
582
        }
583

584
        @Override
585
        public Job createJobForNMPIJob(String owner, int nmpiJobId,
586
                        CreateDescriptor descriptor, String machineName, List<String> tags,
587
                        Duration keepaliveInterval,        byte[] originalRequest) {
588
                var collab = quotaManager.mayUseNMPIJob(owner, nmpiJobId);
×
589
                if (collab.isEmpty()) {
×
590
                        throw new IllegalArgumentException("User cannot create session in "
×
591
                                        + "NMPI job" + nmpiJobId);
592
                }
593
                var quotaDetails = collab.get();
×
594

595
                var job = execute(conn -> createJobInGroup(
×
596
                                owner, quotaDetails.collabId, descriptor, machineName,
597
                                tags, keepaliveInterval, originalRequest));
598

599
                quotaManager.associateNMPIJob(job.getId(), nmpiJobId,
×
600
                                quotaDetails.quotaUnits);
601

602
                // Return the job created
603
                return job;
×
604
        }
605

606
        /**
607
         * Get the specified group.
608
         *
609
         * @param conn
610
         *            DB connection
611
         * @param user
612
         *            Who is the user?
613
         * @param groupName
614
         *            What group did they specify? (May be {@code null} to say "pick
615
         *            the unique valid possibility for the owner".)
616
         * @return The group ID.
617
         * @throws GroupsException
618
         *             If we can't get a definite group to account against.
619
         */
620
        private int selectGroup(Connection conn, String user, String groupName) {
621
                try (var getGroup = conn.query(GET_GROUP_BY_NAME_AND_MEMBER)) {
2✔
622
                        return getGroup.call1(integer("group_id"), user, groupName)
2✔
623
                                        .orElseThrow(() -> new NoSuchGroupException(
2✔
624
                                                        "group %s does not exist or %s "
625
                                                                        + "is not a member of it",
626
                                                        groupName, user));
627
                }
628
        }
629

630
        private String getOnlyGroup(Connection conn, String user) {
631
                var isInternal = conn.query(GET_USER_DETAILS_BY_NAME).call1(
2✔
632
                                (row) -> row.getInt("is_internal") == 1, user).orElseThrow();
2!
633

634
                // OIDC users can use a private group
635
                log.info("User {} is {}internal", user, isInternal ? "" : "not ");
2!
636
                if (!isInternal) {
2!
637
                        return PRIVATE_COLLAB_PREFIX + user;
×
638
                }
639

640
                try (var listGroups = conn.query(GET_GROUP_NAMES_OF_USER)) {
2✔
641
                        // No name given; need to guess.
642
                        var groups = listGroups.call(row -> row.getString("group_name"),
2✔
643
                                        user);
644
                        if (groups.size() > 1) {
2!
645
                                throw new NoSuchGroupException(
×
646
                                                "User is a member of more than one group, so the group"
647
                                                + " must be selected in the request");
648
                        }
649
                        if (groups.size() == 0) {
2!
650
                                throw new NoSuchGroupException(
×
651
                                                "User is not a member of any group!");
652
                        }
653
                        return groups.get(0);
2✔
654
                }
655
        }
656

657
        private static Optional<Integer> getUser(Connection conn, String userName) {
658
                try (var getUser = conn.query(GET_USER_ID)) {
2✔
659
                        return getUser.call1(integer("user_id"), userName);
2✔
660
                }
661
        }
662

663
        private class BoardLocated {
664
                private int boardId;
665

666
                private int z;
667

668
                BoardLocated(Row row) {
2✔
669
                        boardId = row.getInt("board_id");
2✔
670
                        z = row.getInt("z");
2✔
671
                }
2✔
672
        }
673

674
        /**
675
         * Resolve a machine name and {@link HasBoardCoords} to a board identifier.
676
         *
677
         * @param conn
678
         *            How to get to the DB.
679
         * @param machineName
680
         *            The name of the machine.
681
         * @param b
682
         *            The request that is the coordinate holder.
683
         * @param requireTriadRoot
684
         *            Whether we require the Z coordinate to be zero.
685
         * @return The board ID.
686
         * @throws IllegalArgumentException
687
         *             If the board doesn't exist or it is a board that is not a
688
         *             root of a triad when a triad root is required.
689
         */
690
        private Integer locateBoard(Connection conn, String machineName,
691
                        HasBoardCoords b, boolean requireTriadRoot) {
692
                try (var findTriad = conn.query(FIND_BOARD_BY_NAME_AND_XYZ);
2✔
693
                                var findPhysical = conn.query(FIND_BOARD_BY_NAME_AND_CFB);
2✔
694
                                var findIP = conn.query(FIND_BOARD_BY_NAME_AND_IP_ADDRESS)) {
2✔
695
                        if (nonNull(b.triad)) {
2✔
696
                                return findTriad.call1(BoardLocated::new,
2✔
697
                                                machineName, b.triad.x, b.triad.y, b.triad.z)
2✔
698
                                                .filter(board -> !requireTriadRoot || board.z == 0)
2!
699
                                                .map(board -> board.boardId)
2✔
700
                                                .orElseThrow(() -> new IllegalArgumentException(
2✔
701
                                                                NO_BOARD_MSG));
702
                        } else if (nonNull(b.physical)) {
2!
703
                                return findPhysical.call1(
×
704
                                                BoardLocated::new, machineName, b.physical.c,
×
705
                                                                b.physical.f, b.physical.b)
×
706
                                                .filter(board -> !requireTriadRoot || board.z == 0)
×
707
                                                .map(board -> board.boardId)
×
708
                                                .orElseThrow(() -> new IllegalArgumentException(
×
709
                                                                NO_BOARD_MSG));
710
                        } else {
711
                                return findIP.call1(BoardLocated::new, machineName, b.ip)
2✔
712
                                                .filter(board -> !requireTriadRoot || board.z == 0)
2!
713
                                                .map(board -> board.boardId)
2✔
714
                                                .orElseThrow(() -> new IllegalArgumentException(
2✔
715
                                                                NO_BOARD_MSG));
716
                        }
717
                }
2!
718
        }
719

720
        private static Optional<Integer> insertJob(Connection conn, MachineImpl m,
721
                        int owner, int group, Duration keepaliveInterval, byte[] req) {
722
                try (var makeJob = conn.update(INSERT_JOB)) {
2✔
723
                        return makeJob.key(m.id, owner, group, keepaliveInterval, req);
2✔
724
                }
725
        }
726

727
        private Optional<MachineImpl> selectMachine(Connection conn,
728
                        CreateDescriptor descriptor, String machineName,
729
                        List<String> tags) {
730
                if (nonNull(machineName)) {
2!
731
                        var m = getMachine(machineName, false, conn);
2✔
732
                        if (m.isPresent() && isAllocPossible(conn, descriptor, m.get())) {
2!
733
                                return m;
2✔
734
                        }
735
                        return Optional.empty();
×
736
                }
737

738
                if (!tags.isEmpty()) {
×
739
                        for (var m : getMachines(conn, false).values()) {
×
740
                                var mi = (MachineImpl) m;
×
741
                                if (mi.tags.containsAll(tags)
×
742
                                                && isAllocPossible(conn, descriptor, mi)) {
×
743
                                        return Optional.of(mi);
×
744
                                }
745
                        }
×
746
                }
747
                return Optional.empty();
×
748
        }
749

750
        private boolean isAllocPossible(final Connection conn,
751
                        final CreateDescriptor descriptor,
752
                        final MachineImpl m) {
753
                return descriptor.visit(new CreateVisitor<Boolean>() {
2✔
754
                        @Override
755
                        public Boolean numBoards(CreateNumBoards nb) {
756
                                try (var getNBoards = conn.query(COUNT_FUNCTIONING_BOARDS)) {
2✔
757
                                        var numBoards = getNBoards.call1(integer("c"), m.id)
2✔
758
                                                        .orElseThrow();
2✔
759
                                        return numBoards >= nb.numBoards;
2!
760
                                }
761
                        }
762

763
                        @Override
764
                        public Boolean dimensions(CreateDimensions d) {
765
                                try (var checkPossible = conn.query(checkRectangle)) {
2✔
766
                                        return checkPossible.call1((r) -> true, d.width, d.height,
2✔
767
                                                        m.id, d.maxDead).isPresent();
2✔
768
                                }
769
                        }
770

771
                        @Override
772
                        public Boolean dimensionsAt(CreateDimensionsAt da) {
773
                                try (var checkPossible = conn.query(checkRectangleAt)) {
2✔
774
                                        int board = locateBoard(conn, m.name, da, true);
2✔
775
                                        return checkPossible.call1((r) -> true, board,
2✔
776
                                                        da.width, da.height, m.id, da.maxDead).isPresent();
2✔
777
                                } catch (IllegalArgumentException e) {
×
778
                                        // This means the board doesn't exist on the given machine
779
                                        return false;
×
780
                                }
781
                        }
782

783
                        @Override
784
                        public Boolean board(CreateBoard b) {
785
                                try (var check = conn.query(CHECK_LOCATION)) {
2✔
786
                                        int board = locateBoard(conn, m.name, b, false);
2✔
787
                                        return check.call1((r) -> true, m.id, board).isPresent();
2✔
788
                                } catch (IllegalArgumentException e) {
×
789
                                        // This means the board doesn't exist on the given machine
790
                                        return false;
×
791
                                }
792
                        }
793
                });
794
        }
795

796
        @Override
797
        public void purgeDownCache() {
798
                synchronized (this) {
×
799
                        downBoardsCache.clear();
×
800
                        downLinksCache.clear();
×
801
                }
×
802
        }
×
803

804
        private static String mergeDescription(HasChipLocation coreLocation,
805
                        String description) {
806
                if (isNull(description)) {
2!
807
                        description = "<null>";
×
808
                }
809
                if (coreLocation instanceof HasCoreLocation) {
2!
810
                        var loc = (HasCoreLocation) coreLocation;
×
811
                        description += format(" (at core %d of chip %s)", loc.getP(),
×
812
                                        loc.asChipLocation());
×
813
                } else if (nonNull(coreLocation)) {
2!
814
                        description +=
×
815
                                        format(" (at chip %s)", coreLocation.asChipLocation());
×
816
                }
817
                return description;
2✔
818
        }
819

820
        private class Problem {
821
                private int boardId;
822

823
                private Integer jobId;
824

825
                Problem(Row row) {
2✔
826
                        boardId = row.getInt("board_id");
2✔
827
                        jobId = row.getInt("job_id");
2✔
828
                }
2✔
829
        }
830

831
        @Override
832
        public void reportProblem(String address, HasChipLocation coreLocation,
833
                        String description, Permit permit) {
834
                try (var sql = new BoardReportSQL()) {
2✔
835
                        var desc = mergeDescription(coreLocation, description);
2✔
836
                        var email = sql.transaction(() -> {
2✔
837
                                var machines = getMachines(sql.getConnection(), true).values();
2✔
838
                                for (var m : machines) {
2✔
839
                                        var mail = sql.findBoardNet.call1(
2✔
840
                                                        Problem::new, m.getId(), address)
2✔
841
                                                        .flatMap(prob -> reportProblem(prob, desc, permit,
2✔
842
                                                                        sql));
843
                                        if (mail.isPresent()) {
2!
844
                                                return mail;
×
845
                                        }
846
                                }
2✔
847
                                return Optional.empty();
2✔
848
                        });
849
                        // Outside the transaction!
850
                        email.ifPresent(emailSender::sendServiceMail);
2✔
851
                } catch (ReportRollbackExn e) {
×
852
                        log.warn("failed to handle problem report", e);
×
853
                }
2✔
854
        }
2✔
855

856
        private Optional<EmailBuilder> reportProblem(Problem problem,
857
                        String description,        Permit permit, BoardReportSQL sql) {
858
                var email = new EmailBuilder(problem.jobId);
2✔
859
                email.header(description, 1, permit.name);
2✔
860
                int userId = getUser(sql.getConnection(), permit.name).orElseThrow(
2✔
861
                                () -> new ReportRollbackExn("no such user: %s", permit.name));
×
862
                sql.insertReport.key(problem.boardId, problem.jobId,
2✔
863
                                description, userId).ifPresent(email::issue);
2✔
864
                return takeBoardsOutOfService(sql, email).map(acted -> {
2✔
865
                        email.footer(acted);
×
866
                        return email;
×
867
                });
868
        }
869

870
        private class Reported {
871
                private int boardId;
872

873
                private int x;
874

875
                private int y;
876

877
                private int z;
878

879
                private String address;
880

881
                private int numReports;
882

883
                Reported(Row row) {
×
884
                        boardId = row.getInt("board_id");
×
885
                        x = row.getInt("x");
×
886
                        y = row.getInt("y");
×
887
                        z = row.getInt("z");
×
888
                        address = row.getString("address");
×
889
                        numReports = row.getInt("numReports");
×
890
                }
×
891

892
        }
893

894
        /**
895
         * Take boards out of service if they've been reported frequently enough.
896
         *
897
         * @param sql
898
         *            How to touch the DB
899
         * @param email
900
         *            The email we are building.
901
         * @return The number of boards taken out of service
902
         */
903
        private Optional<Integer> takeBoardsOutOfService(BoardReportSQL sql,
904
                        EmailBuilder email) {
905
                int acted = 0;
2✔
906
                for (var report : sql.getReported.call(Reported::new,
2!
907
                                props.getReportActionThreshold())) {
2✔
908
                        if (sql.setFunctioning.call(false, report.boardId) > 0) {
×
909
                                email.serviceActionDone(report);
×
910
                                acted++;
×
911
                        }
912
                }
×
913
                if (acted > 0) {
2!
914
                        purgeDownCache();
×
915
                }
916
                return acted > 0 ? Optional.of(acted) : Optional.empty();
2!
917
        }
918

919
        private static DownLink makeDownLinkFromRow(Row row) {
920
                // Non-standard column names to reduce number of queries
921
                var board1 = new BoardCoords(row.getInt("board_1_x"),
×
922
                                row.getInt("board_1_y"), row.getInt("board_1_z"),
×
923
                                row.getInt("board_1_c"), row.getInt("board_1_f"),
×
924
                                row.getInteger("board_1_b"), row.getString("board_1_addr"));
×
925
                var board2 = new BoardCoords(row.getInt("board_2_x"),
×
926
                                row.getInt("board_2_y"), row.getInt("board_2_z"),
×
927
                                row.getInt("board_2_c"), row.getInt("board_2_f"),
×
928
                                row.getInteger("board_2_b"), row.getString("board_2_addr"));
×
929
                return new DownLink(board1, row.getEnum("dir_1", Direction.class),
×
930
                                board2, row.getEnum("dir_2", Direction.class));
×
931
        }
932

933
        public void emergencyStop(String commandCode) {
934
                if (!commandCode.equals(props.getEmergencyStopCommandCode())) {
×
935
                        throw new IllegalArgumentException("Invalid emergency stop code");
×
936
                }
937
                allocator.emergencyStop();
×
938
                emergencyStop = true;
×
939
                log.warn("Emergency stop requested!");
×
940
        }
×
941

942
        private class MachineImpl implements Machine {
943
                private final int id;
944

945
                private final boolean inService;
946

947
                private final String name;
948

949
                private final Set<String> tags;
950

951
                private final int width;
952

953
                private final int height;
954

955
                private boolean lookedUpWraps;
956

957
                private boolean hWrap;
958

959
                private boolean vWrap;
960

961
                @JsonIgnore
962
                private final Epoch epoch;
963

964
                MachineImpl(Connection conn, Row rs) {
2✔
965
                        id = rs.getInt("machine_id");
2✔
966
                        name = rs.getString("machine_name");
2✔
967
                        width = rs.getInt("width");
2✔
968
                        height = rs.getInt("height");
2✔
969
                        inService = rs.getBoolean("in_service");
2✔
970
                        lookedUpWraps = false;
2✔
971
                        try (var getTags = conn.query(GET_TAGS)) {
2✔
972
                                tags = Row.stream(copy(getTags.call(string("tag"), id)))
2✔
973
                                                .toSet();
2✔
974
                        }
975

976
                        this.epoch = epochs.getMachineEpoch(id);
2✔
977
                }
2✔
978

979
                private int getArea() {
980
                        return width * height * TRIAD_DEPTH;
2✔
981
                }
982

983
                @Override
984
                public boolean waitForChange(Duration timeout) {
985
                        if (isNull(epoch)) {
2!
986
                                log.info("Machine {} epoch is null!", id);
×
987
                                return true;
×
988
                        }
989
                        try {
990
                                log.info("Waiting for change in epoch for {}", id);
2✔
991
                                return epoch.waitForChange(timeout);
×
992
                        } catch (InterruptedException interrupted) {
2✔
993
                                log.info("Interrupted waiting for change on {}", id);
2✔
994
                                return false;
2✔
995
                        }
996
                }
997

998
                @Override
999
                public Optional<BoardLocation> getBoardByChip(HasChipLocation chip) {
1000
                        try (var conn = getConnection();
2✔
1001
                                        var findBoard = conn.query(findBoardByGlobalChip)) {
2✔
1002
                                return conn.transaction(false,
2✔
1003
                                                () -> findBoard.call1(
2✔
1004
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1005
                                                                chip.getX(), chip.getY()));
2✔
1006
                        }
1007
                }
1008

1009
                @Override
1010
                public Optional<BoardLocation> getBoardByPhysicalCoords(
1011
                                PhysicalCoords coords) {
1012
                        try (var conn = getConnection();
2✔
1013
                                        var findBoard = conn.query(findBoardByPhysicalCoords)) {
2✔
1014
                                return conn.transaction(false,
2✔
1015
                                                () -> findBoard.call1(
2✔
1016
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1017
                                                                coords.c, coords.f, coords.b));
2✔
1018
                        }
1019
                }
1020

1021
                @Override
1022
                public Optional<BoardLocation> getBoardByLogicalCoords(
1023
                                TriadCoords coords) {
1024
                        try (var conn = getConnection();
2✔
1025
                                        var findBoard = conn.query(findBoardByLogicalCoords)) {
2✔
1026
                                return conn.transaction(false,
2✔
1027
                                                () -> findBoard.call1(
2✔
1028
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1029
                                                                coords.x, coords.y, coords.z));
2✔
1030
                        }
1031
                }
1032

1033
                @Override
1034
                public Optional<BoardLocation> getBoardByIPAddress(String address) {
1035
                        try (var conn = getConnection();
2✔
1036
                                        var findBoard = conn.query(findBoardByIPAddress)) {
2✔
1037
                                return conn.transaction(false,
2✔
1038
                                                () -> findBoard.call1(
2✔
1039
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1040
                                                                address));
1041
                        }
1042
                }
1043

1044
                @Override
1045
                public String getRootBoardBMPAddress() {
1046
                        try (var conn = getConnection();
2✔
1047
                                        var rootBMPaddr = conn.query(GET_ROOT_BMP_ADDRESS)) {
2✔
1048
                                return conn.transaction(false, () -> rootBMPaddr.call1(
2✔
1049
                                                string("address"), id).orElse(null));
2✔
1050
                        }
1051
                }
1052

1053
                @Override
1054
                public List<Integer> getBoardNumbers() {
1055
                        try (var conn = getConnection();
2✔
1056
                                        var boardNumbers = conn.query(GET_BOARD_NUMBERS)) {
2✔
1057
                                return conn.transaction(false, () -> boardNumbers.call(
2✔
1058
                                                integer("board_num"), id));
2✔
1059
                        }
1060
                }
1061

1062
                @Override
1063
                public List<BoardCoords> getDeadBoards() {
1064
                        // Assume that the list doesn't change for the duration of this obj
1065
                        synchronized (Spalloc.this) {
2✔
1066
                                var down = downBoardsCache.get(name);
2✔
1067
                                if (nonNull(down)) {
2✔
1068
                                        return copy(down);
2✔
1069
                                }
1070
                        }
2✔
1071
                        try (var conn = getConnection();
2✔
1072
                                        var boardNumbers = conn.query(GET_DEAD_BOARDS)) {
2✔
1073
                                var downBoards = conn.transaction(false,
2✔
1074
                                                () -> boardNumbers.call(
2✔
1075
                                                                row -> new BoardCoords(row, false), id));
2✔
1076
                                synchronized (Spalloc.this) {
2✔
1077
                                        downBoardsCache.putIfAbsent(name, downBoards);
2✔
1078
                                }
2✔
1079
                                return copy(downBoards);
2✔
1080
                        }
1081
                }
1082

1083
                @Override
1084
                public List<DownLink> getDownLinks() {
1085
                        // Assume that the list doesn't change for the duration of this obj
1086
                        synchronized (Spalloc.this) {
2✔
1087
                                var down = downLinksCache.get(name);
2✔
1088
                                if (nonNull(down)) {
2✔
1089
                                        return copy(down);
2✔
1090
                                }
1091
                        }
2✔
1092
                        try (var conn = getConnection();
2✔
1093
                                        var boardNumbers = conn.query(getDeadLinks)) {
2✔
1094
                                var downLinks = conn.transaction(false, () -> boardNumbers
2✔
1095
                                                .call(Spalloc::makeDownLinkFromRow, id));
2✔
1096
                                synchronized (Spalloc.this) {
2✔
1097
                                        downLinksCache.putIfAbsent(name, downLinks);
2✔
1098
                                }
2✔
1099
                                return copy(downLinks);
2✔
1100
                        }
1101
                }
1102

1103
                @Override
1104
                public List<Integer> getAvailableBoards() {
1105
                        try (var conn = getConnection();
2✔
1106
                                        var boardNumbers = conn
2✔
1107
                                                        .query(GET_AVAILABLE_BOARD_NUMBERS)) {
2✔
1108
                                return conn.transaction(false, () -> boardNumbers.call(
2✔
1109
                                                integer("board_num"), id));
2✔
1110
                        }
1111
                }
1112

1113
                @Override
1114
                public int getId() {
1115
                        return id;
2✔
1116
                }
1117

1118
                @Override
1119
                public String getName() {
1120
                        return name;
2✔
1121
                }
1122

1123
                @Override
1124
                public Set<String> getTags() {
1125
                        return tags;
2✔
1126
                }
1127

1128
                @Override
1129
                public int getWidth() {
1130
                        return width;
2✔
1131
                }
1132

1133
                @Override
1134
                public int getHeight() {
1135
                        return height;
2✔
1136
                }
1137

1138
                @Override
1139
                public boolean isInService() {
1140
                        return inService;
2✔
1141
                }
1142

1143
                @Override
1144
                public String getBMPAddress(BMPCoords bmp) {
1145
                        try (var conn = getConnection();
2✔
1146
                                        var bmpAddr = conn.query(GET_BMP_ADDRESS)) {
2✔
1147
                                return conn.transaction(false,
2✔
1148
                                                () -> bmpAddr
1149
                                                                .call1(string("address"), id, bmp.getCabinet(),
2✔
1150
                                                                                bmp.getFrame()).orElse(null));
2✔
1151
                        }
1152
                }
1153

1154
                @Override
1155
                public List<Integer> getBoardNumbers(BMPCoords bmp) {
1156
                        try (var conn = getConnection();
2✔
1157
                                        var boardNumbers = conn.query(GET_BMP_BOARD_NUMBERS)) {
2✔
1158
                                return conn.transaction(false,
2✔
1159
                                                () -> boardNumbers
2✔
1160
                                                                .call(integer("board_num"), id,
2✔
1161
                                                                                bmp.getCabinet(), bmp.getFrame()));
2✔
1162
                        }
1163
                }
1164

1165
                @Override
1166
                public boolean equals(Object other) {
1167
                        // Equality is defined exactly by the database ID
1168
                        return (other instanceof MachineImpl)
×
1169
                                        && (id == ((MachineImpl) other).id);
1170
                }
1171

1172
                @Override
1173
                public int hashCode() {
1174
                        return id;
×
1175
                }
1176

1177
                @Override
1178
                public String toString() {
1179
                        return "Machine(" + name + ")";
×
1180
                }
1181

1182
                private void retrieveWraps() {
1183
                        try (var conn = getConnection();
×
1184
                                        var getWraps = conn.query(GET_MACHINE_WRAPS)) {
×
1185
                                /*
1186
                                 * No locking; not too bothered which thread asks as result will
1187
                                 * be the same either way
1188
                                 */
1189
                                lookedUpWraps =
×
1190
                                                conn.transaction(false, () -> getWraps.call1(rs -> {
×
1191
                                                        hWrap = rs.getBoolean("horizontal_wrap");
×
1192
                                                        vWrap = rs.getBoolean("vertical_wrap");
×
1193
                                                        return true;
×
1194
                                                }, id)).orElse(false);
×
1195
                        }
1196
                }
×
1197

1198
                @Override
1199
                public boolean isHorizonallyWrapped() {
1200
                        if (!lookedUpWraps) {
×
1201
                                retrieveWraps();
×
1202
                        }
1203
                        return hWrap;
×
1204
                }
1205

1206
                @Override
1207
                public boolean isVerticallyWrapped() {
1208
                        if (!lookedUpWraps) {
×
1209
                                retrieveWraps();
×
1210
                        }
1211
                        return vWrap;
×
1212
                }
1213
        }
1214

1215
        private final class JobCollection implements Jobs {
1216
                @JsonIgnore
1217
                private final Epoch epoch;
1218

1219
                private final List<Job> jobs;
1220

1221
                private JobCollection(List<Job> jobs) {
2✔
1222
                        this.jobs = jobs;
2✔
1223
                        if (jobs.isEmpty()) {
2✔
1224
                                epoch = null;
2✔
1225
                        } else {
1226
                                epoch = epochs.getJobsEpoch(
2✔
1227
                                                jobs.stream().map(Job::getId).collect(toList()));
2✔
1228
                        }
1229
                }
2✔
1230

1231
                @Override
1232
                public boolean waitForChange(Duration timeout) {
1233
                        if (isNull(epoch)) {
×
1234
                                return true;
×
1235
                        }
1236
                        try {
1237
                                return epoch.waitForChange(timeout);
×
1238
                        } catch (InterruptedException interrupted) {
×
1239
                                currentThread().interrupt();
×
1240
                                return false;
×
1241
                        }
1242
                }
1243

1244
                /**
1245
                 * Get the set of jobs changed.
1246
                 *
1247
                 * @param timeout
1248
                 *            The timeout to wait for until something happens.
1249
                 * @return The set of changed job identifiers.
1250
                 */
1251
                @Override
1252
                public Collection<Integer> getChanged(Duration timeout) {
1253
                        if (isNull(epoch)) {
2!
1254
                                return jobs.stream().map(Job::getId).collect(toSet());
2✔
1255
                        }
1256
                        try {
1257
                                return epoch.getChanged(timeout);
×
1258
                        } catch (InterruptedException interrupted) {
×
1259
                                currentThread().interrupt();
×
1260
                                return jobs.stream().map(Job::getId).collect(toSet());
×
1261
                        }
1262
                }
1263

1264
                @Override
1265
                public List<Job> jobs() {
1266
                        return copy(jobs);
×
1267
                }
1268

1269
                @Override
1270
                public List<Integer> ids() {
1271
                        return jobs.stream().map(Job::getId).collect(toList());
2✔
1272
                }
1273
        }
1274

1275
        private final class BoardReportSQL extends AbstractSQL {
2✔
1276
                private final Query findBoardByChip = conn.query(findBoardByJobChip);
2✔
1277

1278
                private final Query findBoardByTriad = conn.query(
2✔
1279
                                findBoardByLogicalCoords);
2✔
1280

1281
                private final Query findBoardPhys = conn.query(
2✔
1282
                                findBoardByPhysicalCoords);
2✔
1283

1284
                private final Query findBoardNet = conn.query(findBoardByIPAddress);
2✔
1285

1286
                private final Update insertReport = conn.update(INSERT_BOARD_REPORT);
2✔
1287

1288
                private final Query getReported = conn.query(getReportedBoards);
2✔
1289

1290
                private final Update setFunctioning = conn.update(
2✔
1291
                                SET_FUNCTIONING_FIELD);
1292

1293
                private final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
2✔
1294

1295
                @Override
1296
                public void close() {
1297
                        findBoardByChip.close();
2✔
1298
                        findBoardByTriad.close();
2✔
1299
                        findBoardPhys.close();
2✔
1300
                        findBoardNet.close();
2✔
1301
                        insertReport.close();
2✔
1302
                        getReported.close();
2✔
1303
                        setFunctioning.close();
2✔
1304
                        getNamedMachine.close();
2✔
1305
                        super.close();
2✔
1306
                }
2✔
1307
        }
1308

1309
        /** Used to assemble an issue-report email for sending. */
1310
        private static final class EmailBuilder {
1311
                /**
1312
                 * More efficient than several String.format() calls, and much clearer
1313
                 * than a mess of direct {@link StringBuilder} calls!
1314
                 */
1315
                private final Formatter b = new Formatter(Locale.UK);
2✔
1316

1317
                private final int id;
1318

1319
                /**
1320
                 * @param id
1321
                 *            The job ID
1322
                 */
1323
                EmailBuilder(int id) {
2✔
1324
                        this.id = id;
2✔
1325
                }
2✔
1326

1327
                void header(String issue, int numBoards, String who) {
1328
                        b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
2✔
1329
                                        numBoards, who);
2✔
1330
                }
2✔
1331

1332
                void chip(ReportedBoard board) {
1333
                        b.format("\tBoard for job (%d) chip %s\n", //
×
1334
                                        id, board.chip);
×
1335
                }
×
1336

1337
                void triad(ReportedBoard board) {
1338
                        b.format("\tBoard for job (%d) board (X:%d,Y:%d,Z:%d)\n", //
×
1339
                                        id, board.x, board.y, board.z);
×
1340
                }
×
1341

1342
                void phys(ReportedBoard board) {
1343
                        b.format(
×
1344
                                        "\tBoard for job (%d) board "
1345
                                                        + "[Cabinet:%d,Frame:%d,Board:%d]\n", //
1346
                                        id, board.cabinet, board.frame, board.board);
×
1347
                }
×
1348

1349
                void ip(ReportedBoard board) {
1350
                        b.format("\tBoard for job (%d) board (IP: %s)\n", //
2✔
1351
                                        id, board.address);
2✔
1352
                }
2✔
1353

1354
                void issue(int issueId) {
1355
                        b.format("\t\tAction: noted as issue #%d\n", //
2✔
1356
                                        issueId);
2✔
1357
                }
2✔
1358

1359
                void footer(int numActions) {
1360
                        b.format("\nSummary: %d boards taken out of service.\n",
×
1361
                                        numActions);
×
1362
                }
×
1363

1364
                void serviceActionDone(Reported report) {
1365
                        b.format(
×
1366
                                        "\tAction: board (X:%d,Y:%d,Z:%d) (IP: %s) "
1367
                                                        + "taken out of service once not in use "
1368
                                                        + "(%d problems reported)\n",
1369
                                        report.x, report.y, report.z,
×
1370
                                        report.address, report.numReports);
×
1371
                }
×
1372

1373
                /** @return The assembled message body. */
1374
                @Override
1375
                public String toString() {
1376
                        return b.toString();
×
1377
                }
1378
        }
1379

1380
        private final class JobImpl implements Job {
1381
                @JsonIgnore
1382
                private Epoch epoch;
1383

1384
                private final int id;
1385

1386
                private final int machineId;
1387

1388
                private Integer width;
1389

1390
                private Integer height;
1391

1392
                private Integer depth;
1393

1394
                private JobState state;
1395

1396
                /** If not {@code null}, the ID of the root board of the job. */
1397
                private Integer root;
1398

1399
                private ChipLocation chipRoot;
1400

1401
                private String owner;
1402

1403
                private String keepaliveHost;
1404

1405
                private Instant startTime;
1406

1407
                private Instant keepaliveTime;
1408

1409
                private Instant finishTime;
1410

1411
                private String deathReason;
1412

1413
                private byte[] request;
1414

1415
                private boolean partial;
1416

1417
                private MachineImpl cachedMachine;
1418

1419
                JobImpl(int id, int machineId) {
2✔
1420
                        this.epoch = epochs.getJobsEpoch(id);
2✔
1421
                        this.id = id;
2✔
1422
                        this.machineId = machineId;
2✔
1423
                        partial = true;
2✔
1424
                }
2✔
1425

1426
                JobImpl(int jobId, int machineId, JobState jobState,
1427
                                Instant keepalive) {
1428
                        this(jobId, machineId);
2✔
1429
                        state = jobState;
2✔
1430
                        keepaliveTime = keepalive;
2✔
1431
                }
2✔
1432

1433
                JobImpl(Connection conn, Row row) {
2✔
1434
                        this.id = row.getInt("job_id");
2✔
1435
                        this.machineId = row.getInt("machine_id");
2✔
1436
                        width = row.getInteger("width");
2✔
1437
                        height = row.getInteger("height");
2✔
1438
                        depth = row.getInteger("depth");
2✔
1439
                        root = row.getInteger("root_id");
2✔
1440
                        owner = row.getString("owner");
2✔
1441
                        if (nonNull(root)) {
2✔
1442
                                try (var boardRoot = conn.query(GET_ROOT_OF_BOARD)) {
2✔
1443
                                        chipRoot = boardRoot.call1(chip("root_x", "root_y"), root)
2✔
1444
                                                        .orElse(null);
2✔
1445
                                }
1446
                        }
1447
                        state = row.getEnum("job_state", JobState.class);
2✔
1448
                        keepaliveHost = row.getString("keepalive_host");
2✔
1449
                        keepaliveTime = row.getInstant("keepalive_timestamp");
2✔
1450
                        startTime = row.getInstant("create_timestamp");
2✔
1451
                        finishTime = row.getInstant("death_timestamp");
2✔
1452
                        deathReason = row.getString("death_reason");
2✔
1453
                        request = row.getBytes("original_request");
2✔
1454
                        partial = false;
2✔
1455

1456
                        this.epoch = epochs.getJobsEpoch(id);
2✔
1457
                }
2✔
1458

1459
                /**
1460
                 * Get the machine that this job is running on. May used a cached value.
1461
                 * A transaction is required, but may be a read-only transaction.
1462
                 *
1463
                 * @param conn
1464
                 *            The connection to the DB
1465
                 * @return The overall machine handle.
1466
                 */
1467
                private synchronized MachineImpl getJobMachine(Connection conn) {
1468
                        if (cachedMachine == null || !cachedMachine.epoch.isValid()) {
2!
1469
                                cachedMachine = Spalloc.this.getMachine(machineId, true, conn)
2✔
1470
                                                .orElseThrow();
2✔
1471
                        }
1472
                        return cachedMachine;
2✔
1473
                }
1474

1475
                @Override
1476
                public void access(String keepaliveAddress) {
1477
                        if (partial) {
2!
1478
                                throw new PartialJobException();
×
1479
                        }
1480
                        try (var conn = getConnection();
2✔
1481
                                        var keepAlive = conn.update(UPDATE_KEEPALIVE)) {
2✔
1482
                                conn.transaction(() -> keepAlive.call(keepaliveAddress, id));
2✔
1483
                        }
1484
                }
2✔
1485

1486
                @Override
1487
                public void destroy(String reason) {
1488
                        if (partial) {
2!
1489
                                throw new PartialJobException();
×
1490
                        }
1491
                        powerController.destroyJob(id, reason);
2✔
1492
                        rememberer.closeJob(id);
2✔
1493
                }
2✔
1494

1495
                @Override
1496
                public void setPower(boolean power) {
1497
                        powerController.setPower(id, power ? ON : OFF, READY);
×
1498
                }
×
1499

1500
                @Override
1501
                public boolean waitForChange(Duration timeout) {
1502
                        if (isNull(epoch)) {
2!
1503
                                return true;
×
1504
                        }
1505
                        try {
1506
                                return epoch.waitForChange(timeout);
×
1507
                        } catch (InterruptedException interrupted) {
2✔
1508
                                currentThread().interrupt();
2✔
1509
                                return false;
2✔
1510
                        }
1511
                }
1512

1513
                @Override
1514
                public int getId() {
1515
                        return id;
2✔
1516
                }
1517

1518
                @Override
1519
                public JobState getState() {
1520
                        return state;
2✔
1521
                }
1522

1523
                @Override
1524
                public Instant getStartTime() {
1525
                        return startTime;
2✔
1526
                }
1527

1528
                @Override
1529
                public Optional<Instant> getFinishTime() {
1530
                        return Optional.ofNullable(finishTime);
2✔
1531
                }
1532

1533
                @Override
1534
                public Optional<String> getReason() {
1535
                        return Optional.ofNullable(deathReason);
2✔
1536
                }
1537

1538
                @Override
1539
                public Optional<String> getKeepaliveHost() {
1540
                        if (partial) {
2!
1541
                                return Optional.empty();
×
1542
                        }
1543
                        return Optional.ofNullable(keepaliveHost);
2✔
1544
                }
1545

1546
                @Override
1547
                public Instant getKeepaliveTimestamp() {
1548
                        return keepaliveTime;
2✔
1549
                }
1550

1551
                @Override
1552
                public Optional<byte[]> getOriginalRequest() {
1553
                        if (partial) {
2!
1554
                                return Optional.empty();
×
1555
                        }
1556
                        return Optional.ofNullable(request);
2✔
1557
                }
1558

1559
                @Override
1560
                public Optional<SubMachine> getMachine() {
1561
                        if (isNull(root)) {
2✔
1562
                                return Optional.empty();
2✔
1563
                        }
1564
                        return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
2✔
1565
                }
1566

1567
                @Override
1568
                public Optional<BoardLocation> whereIs(int x, int y) {
1569
                        if (isNull(root)) {
2!
1570
                                return Optional.empty();
×
1571
                        }
1572
                        try (var conn = getConnection();
2✔
1573
                                        var findBoard = conn.query(findBoardByJobChip)) {
2✔
1574
                                return conn.transaction(false, () -> findBoard
2✔
1575
                                                .call1(row -> new BoardLocationImpl(row,
2✔
1576
                                                                getJobMachine(conn)), id, root, x, y));
2✔
1577
                        }
1578
                }
1579

1580
                // -------------------------------------------------------------
1581
                // Bad board report handling
1582

1583
                @Override
1584
                public String reportIssue(IssueReportRequest report, Permit permit) {
1585
                        try (var q = new BoardReportSQL()) {
2✔
1586
                                var email = new EmailBuilder(id);
2✔
1587
                                var result = q.transaction(
2✔
1588
                                                () -> reportIssue(report, permit, email, q));
2✔
1589
                                emailSender.sendServiceMail(email);
2✔
1590
                                for (var m : report.boards.stream()
2✔
1591
                                                .map(b -> q.getNamedMachine.call1(
2✔
1592
                                                                r -> r.getInt("machine_id"), b.machine, true))
2✔
1593
                                                .collect(toSet())) {
2✔
1594
                                        if (m.isPresent()) {
2!
1595
                                                epochs.machineChanged(m.get());
×
1596
                                        }
1597
                                }
2✔
1598

1599
                                return result;
2✔
1600
                        } catch (ReportRollbackExn e) {
×
1601
                                return e.getMessage();
×
1602
                        }
1603
                }
1604

1605
                /**
1606
                 * Report an issue with some boards and assemble the email to send. This
1607
                 * may result in boards being taken out of service (i.e., no longer
1608
                 * being available to be allocated; their current allocation will
1609
                 * continue).
1610
                 * <p>
1611
                 * <strong>NB:</strong> The sending of the email sending is
1612
                 * <em>outside</em> the transaction that this code is executed in.
1613
                 *
1614
                 * @param report
1615
                 *            The report from the user.
1616
                 * @param permit
1617
                 *            Who the user is.
1618
                 * @param email
1619
                 *            The email we're assembling.
1620
                 * @param q
1621
                 *            SQL access queries.
1622
                 * @return Summary of action taken message, to go to user.
1623
                 * @throws ReportRollbackExn
1624
                 *             If the report is bad somehow.
1625
                 */
1626
                private String reportIssue(IssueReportRequest report, Permit permit,
1627
                                EmailBuilder email, BoardReportSQL q) throws ReportRollbackExn {
1628
                        email.header(report.issue, report.boards.size(), permit.name);
2✔
1629
                        int userId = getUser(q.getConnection(), permit.name)
2✔
1630
                                        .orElseThrow(() -> new ReportRollbackExn(
2✔
1631
                                                        "no such user: %s", permit.name));
1632
                        for (var board : report.boards) {
2✔
1633
                                addIssueReport(q, getJobBoardForReport(q, board, email),
2✔
1634
                                                report.issue, userId, email);
1635
                        }
2✔
1636
                        return takeBoardsOutOfService(q, email).map(acted -> {
2✔
1637
                                email.footer(acted);
×
1638
                                return format("%d boards taken out of service", acted);
×
1639
                        }).orElse("report noted");
2✔
1640
                }
1641

1642
                /**
1643
                 * Convert a board locator (for an issue report) into a board ID.
1644
                 *
1645
                 * @param q
1646
                 *            How to touch the DB
1647
                 * @param board
1648
                 *            What board are we talking about
1649
                 * @param email
1650
                 *            The email we are building.
1651
                 * @return The board ID
1652
                 * @throws ReportRollbackExn
1653
                 *             If the board can't be converted to an ID
1654
                 */
1655
                private int getJobBoardForReport(BoardReportSQL q, ReportedBoard board,
1656
                                EmailBuilder email) throws ReportRollbackExn {
1657
                        Problem r;
1658
                        if (nonNull(board.chip)) {
2!
1659
                                r = q.findBoardByChip
×
1660
                                                .call1(Problem::new, id, root, board.chip.getX(),
×
1661
                                                                board.chip.getY())
×
1662
                                                .orElseThrow(() -> new ReportRollbackExn(board.chip));
×
1663
                                email.chip(board);
×
1664
                        } else if (nonNull(board.x)) {
2!
1665
                                r = q.findBoardByTriad
×
1666
                                                .call1(Problem::new, machineId, board.x, board.y,
×
1667
                                                                board.z)
1668
                                                .orElseThrow(() -> new ReportRollbackExn(
×
1669
                                                                "triad (%s,%s,%s) not in machine", board.x,
1670
                                                                board.y, board.z));
1671
                                if (isNull(r.jobId) || id != r.jobId) {
×
1672
                                        throw new ReportRollbackExn(
×
1673
                                                        "triad (%s,%s,%s) not allocated to job %d", board.x,
1674
                                                        board.y, board.z, id);
×
1675
                                }
1676
                                email.triad(board);
×
1677
                        } else if (nonNull(board.cabinet)) {
2!
1678
                                r = q.findBoardPhys
×
1679
                                                .call1(Problem::new, machineId, board.cabinet,
×
1680
                                                                board.frame, board.board)
1681
                                                .orElseThrow(() -> new ReportRollbackExn(
×
1682
                                                                "physical board [%s,%s,%s] not in machine",
1683
                                                                board.cabinet, board.frame, board.board));
1684
                                if (isNull(r.jobId) || id != r.jobId) {
×
1685
                                        throw new ReportRollbackExn(
×
1686
                                                        "physical board [%s,%s,%s] not allocated to job %d",
1687
                                                        board.cabinet, board.frame, board.board, id);
×
1688
                                }
1689
                                email.phys(board);
×
1690
                        } else if (nonNull(board.address)) {
2!
1691
                                r = q.findBoardNet.call1(Problem::new, machineId, board.address)
2✔
1692
                                                .orElseThrow(() -> new ReportRollbackExn(
2✔
1693
                                                                "board at %s not in machine", board.address));
1694
                                if (isNull(r.jobId) || id != r.jobId) {
2!
1695
                                        throw new ReportRollbackExn(
×
1696
                                                        "board at %s not allocated to job %d",
1697
                                                        board.address, id);
×
1698
                                }
1699
                                email.ip(board);
2✔
1700
                        } else {
1701
                                throw new UnsupportedOperationException();
×
1702
                        }
1703
                        return r.boardId;
2✔
1704
                }
1705

1706
                /**
1707
                 * Record a reported issue with a board.
1708
                 *
1709
                 * @param u
1710
                 *            How to touch the DB
1711
                 * @param boardId
1712
                 *            What board has the issue?
1713
                 * @param issue
1714
                 *            What is the issue?
1715
                 * @param userId
1716
                 *            Who is doing the report?
1717
                 * @param email
1718
                 *            The email we are building.
1719
                 */
1720
                private void addIssueReport(BoardReportSQL u, int boardId, String issue,
1721
                                int userId, EmailBuilder email) {
1722
                        u.insertReport.key(boardId, id, issue, userId)
2✔
1723
                                        .ifPresent(email::issue);
2✔
1724
                }
2✔
1725

1726
                // -------------------------------------------------------------
1727

1728
                @Override
1729
                public Optional<ChipLocation> getRootChip() {
1730
                        return Optional.ofNullable(chipRoot);
2✔
1731
                }
1732

1733
                @Override
1734
                public Optional<String> getOwner() {
1735
                        if (partial) {
2!
1736
                                return Optional.empty();
×
1737
                        }
1738
                        return Optional.ofNullable(owner);
2✔
1739
                }
1740

1741
                @Override
1742
                public Optional<Integer> getWidth() {
1743
                        return Optional.ofNullable(width);
2✔
1744
                }
1745

1746
                @Override
1747
                public Optional<Integer> getHeight() {
1748
                        return Optional.ofNullable(height);
2✔
1749
                }
1750

1751
                @Override
1752
                public Optional<Integer> getDepth() {
1753
                        return Optional.ofNullable(depth);
2✔
1754
                }
1755

1756
                @Override
1757
                public void rememberProxy(ProxyCore proxy) {
1758
                        rememberer.rememberProxyForJob(id, proxy);
×
1759
                }
×
1760

1761
                @Override
1762
                public void forgetProxy(ProxyCore proxy) {
1763
                        rememberer.removeProxyForJob(id, proxy);
×
1764
                }
×
1765

1766
                @Override
1767
                @SuppressWarnings("MustBeClosed")
1768
                public TransceiverInterface getTransceiver() throws IOException,
1769
                                InterruptedException, SpinnmanException {
1770
                        var mac = getMachine();
×
1771
                        if (mac.isEmpty()) {
×
1772
                                throw new IllegalStateException(
×
1773
                                                "Job is not active!");
1774
                        }
1775
                        var txrx = rememberer.getTransceiverForJob(id);
×
1776
                        if (nonNull(txrx)) {
×
1777
                                return txrx;
×
1778
                        }
1779
                        List<uk.ac.manchester.spinnaker.connections.model.Connection>
1780
                                connections = new ArrayList<>();
×
1781
                        for (var conn : mac.get().getConnections()) {
×
1782
                                connections.add(new SCPConnection(conn.getChip(),
×
1783
                                                null, null, InetAddress.getByName(conn.getHostname())));
×
1784
                        }
×
1785
                        txrx = new Transceiver(MachineVersion.FIVE, connections);
×
1786
                        var unused = txrx.getMachineDetails();
×
1787
                        rememberer.rememberTransceiverForJob(id, txrx);
×
1788
                        return txrx;
×
1789
                }
1790

1791
                @Override
1792
                @SuppressWarnings("MustBeClosed")
1793
                public FastDataIn getFastDataIn(CoreLocation gathererCore, IPTag iptag)
1794
                                throws ProcessException, IOException, InterruptedException {
1795
                        var fdi = rememberer.getFastDataIn(id, iptag.getDestination());
×
1796
                        if (fdi != null) {
×
1797
                                return fdi;
×
1798
                        }
1799
                        fdi = new FastDataIn(gathererCore, iptag);
×
1800
                        rememberer.rememberFastDataIn(id, iptag.getDestination(), fdi);
×
1801
                        return fdi;
×
1802
                }
1803

1804
                @Override
1805
                @SuppressWarnings("MustBeClosed")
1806
                public Downloader getDownloader(IPTag iptag)
1807
                                throws ProcessException, IOException, InterruptedException {
1808
                        var downloader = rememberer.getDownloader(id,
×
1809
                                        iptag.getDestination());
×
1810
                        if (downloader != null) {
×
1811
                                // Ensure the downloader can be reuse
1812
                                downloader.reuse();
×
1813
                                return downloader;
×
1814
                        }
1815
                        downloader = new Downloader(iptag);
×
1816
                        rememberer.rememberDownloader(id, iptag.getDestination(),
×
1817
                                        downloader);
1818
                        return downloader;
×
1819
                }
1820

1821
                @Override
1822
                public boolean equals(Object other) {
1823
                        // Equality is defined exactly by the database ID
1824
                        return (other instanceof JobImpl) && (id == ((JobImpl) other).id);
×
1825
                }
1826

1827
                @Override
1828
                public int hashCode() {
1829
                        return id;
×
1830
                }
1831

1832
                @Override
1833
                public String toString() {
1834
                        return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1835
                                        width, height, depth, startTime, finishTime);
1836
                }
1837

1838
                private final class SubMachineImpl implements SubMachine {
1839
                        /** The machine that this sub-machine is part of. */
1840
                        private final Machine machine;
1841

1842
                        /** The root X coordinate of this sub-machine. */
1843
                        private int rootX;
1844

1845
                        /** The root Y coordinate of this sub-machine. */
1846
                        private int rootY;
1847

1848
                        /** The root Z coordinate of this sub-machine. */
1849
                        private int rootZ;
1850

1851
                        /** The connection details of this sub-machine. */
1852
                        private List<ConnectionInfo> connections;
1853

1854
                        /** The board locations of this sub-machine. */
1855
                        private List<BoardCoordinates> boards;
1856

1857
                        private List<Integer> boardIds;
1858

1859
                        private SubMachineImpl(Connection conn) {
2✔
1860
                                machine = getJobMachine(conn);
2✔
1861
                                try (var getRootXY = conn.query(GET_ROOT_COORDS);
2✔
1862
                                                var getBoardInfo = conn.query(GET_BOARD_CONNECT_INFO)) {
2✔
1863
                                        getRootXY.call1(row -> {
2✔
1864
                                                rootX = row.getInt("x");
2✔
1865
                                                rootY = row.getInt("y");
2✔
1866
                                                rootZ = row.getInt("z");
2✔
1867
                                                // We have to return something,
1868
                                                // but it doesn't matter what
1869
                                                return true;
2✔
1870
                                        }, root);
1871
                                        int capacityEstimate = width * height;
2✔
1872
                                        connections = new ArrayList<>(capacityEstimate);
2✔
1873
                                        boards = new ArrayList<>(capacityEstimate);
2✔
1874
                                        boardIds = new ArrayList<>(capacityEstimate);
2✔
1875
                                        getBoardInfo.call(row -> {
2✔
1876
                                                boardIds.add(row.getInt("board_id"));
2✔
1877
                                                boards.add(new BoardCoordinates(row.getInt("x"),
2✔
1878
                                                                row.getInt("y"), row.getInt("z")));
2✔
1879
                                                connections.add(new ConnectionInfo(
2✔
1880
                                                                relativeChipLocation(row.getInt("root_x"),
2✔
1881
                                                                                row.getInt("root_y")),
2✔
1882
                                                                row.getString("address")));
2✔
1883
                                                // We have to return something,
1884
                                                // but it doesn't matter what
1885
                                                return true;
2✔
1886
                                        }, id);
2✔
1887
                                }
1888
                        }
2✔
1889

1890
                        private ChipLocation relativeChipLocation(int x, int y) {
1891
                                x -= chipRoot.getX();
2✔
1892
                                y -= chipRoot.getY();
2✔
1893
                                // Allow for wrapping
1894
                                if (x < 0) {
2!
1895
                                        x += machine.getWidth() * TRIAD_CHIP_SIZE;
×
1896
                                }
1897
                                if (y < 0) {
2!
1898
                                        y += machine.getHeight() * TRIAD_CHIP_SIZE;
×
1899
                                }
1900
                                return new ChipLocation(x, y);
2✔
1901
                        }
1902

1903
                        @Override
1904
                        public Machine getMachine() {
1905
                                return machine;
2✔
1906
                        }
1907

1908
                        @Override
1909
                        public int getRootX() {
1910
                                return rootX;
2✔
1911
                        }
1912

1913
                        @Override
1914
                        public int getRootY() {
1915
                                return rootY;
2✔
1916
                        }
1917

1918
                        @Override
1919
                        public int getRootZ() {
1920
                                return rootZ;
2✔
1921
                        }
1922

1923
                        @Override
1924
                        public int getWidth() {
1925
                                return width;
2✔
1926
                        }
1927

1928
                        @Override
1929
                        public int getHeight() {
1930
                                return height;
2✔
1931
                        }
1932

1933
                        @Override
1934
                        public int getDepth() {
1935
                                return depth;
2✔
1936
                        }
1937

1938
                        @Override
1939
                        public List<ConnectionInfo> getConnections() {
1940
                                return connections;
2✔
1941
                        }
1942

1943
                        @Override
1944
                        public List<BoardCoordinates> getBoards() {
1945
                                return boards;
2✔
1946
                        }
1947

1948
                        @Override
1949
                        public PowerState getPower() {
1950
                                try (var conn = getConnection();
2✔
1951
                                                var power = conn.query(GET_SUM_BOARDS_POWERED)) {
2✔
1952
                                        return conn.transaction(false,
2✔
1953
                                                        () -> power.call1(integer("total_on"), id)
2✔
1954
                                                                        .map(totalOn -> totalOn < boardIds.size()
2!
1955
                                                                                        ? OFF
2✔
1956
                                                                                        : ON)
×
1957
                                                                        .orElse(null));
2✔
1958
                                }
1959
                        }
1960

1961
                        @Override
1962
                        public void setPower(PowerState ps) {
1963
                                if (partial) {
2!
1964
                                        throw new PartialJobException();
×
1965
                                }
1966
                                powerController.setPower(id, ps, READY);
2✔
1967
                        }
2✔
1968
                }
1969
        }
1970

1971
        /**
1972
         * Board location implementation. Does not retain database connections after
1973
         * creation.
1974
         *
1975
         * @author Donal Fellows
1976
         */
1977
        private final class BoardLocationImpl implements BoardLocation {
1978
                private JobImpl job;
1979

1980
                private final String machineName;
1981

1982
                private final int machineWidth;
1983

1984
                private final int machineHeight;
1985

1986
                private final ChipLocation chip;
1987

1988
                private final ChipLocation boardChip;
1989

1990
                private final BoardCoordinates logical;
1991

1992
                private final BoardPhysicalCoordinates physical;
1993

1994
                // Transaction is open
1995
                private BoardLocationImpl(Row row, Machine machine) {
2✔
1996
                        machineName = row.getString("machine_name");
2✔
1997
                        logical = new BoardCoordinates(row.getInt("x"), row.getInt("y"),
2✔
1998
                                        row.getInt("z"));
2✔
1999
                        physical = new BoardPhysicalCoordinates(row.getInt("cabinet"),
2✔
2000
                                        row.getInt("frame"), row.getInteger("board_num"));
2✔
2001
                        chip = row.getChip("chip_x", "chip_y");
2✔
2002
                        machineWidth = machine.getWidth();
2✔
2003
                        machineHeight = machine.getHeight();
2✔
2004
                        var boardX = row.getInteger("board_chip_x");
2✔
2005
                        if (nonNull(boardX)) {
2!
2006
                                boardChip = row.getChip("board_chip_x", "board_chip_y");
2✔
2007
                        } else {
2008
                                boardChip = chip;
×
2009
                        }
2010

2011
                        var jobId = row.getInteger("job_id");
2✔
2012
                        if (nonNull(jobId)) {
2✔
2013
                                job = new JobImpl(jobId, machine.getId());
2✔
2014
                                job.chipRoot = row.getChip("job_root_chip_x",
2✔
2015
                                                "job_root_chip_y");
2016
                        }
2017
                }
2✔
2018

2019
                @Override
2020
                public ChipLocation getBoardChip() {
2021
                        return boardChip;
2✔
2022
                }
2023

2024
                @Override
2025
                public ChipLocation getChipRelativeTo(ChipLocation rootChip) {
2026
                        int x = chip.getX() - rootChip.getX();
2✔
2027
                        if (x < 0) {
2!
2028
                                x += machineWidth * TRIAD_CHIP_SIZE;
×
2029
                        }
2030
                        int y = chip.getY() - rootChip.getY();
2✔
2031
                        if (y < 0) {
2!
2032
                                y += machineHeight * TRIAD_CHIP_SIZE;
×
2033
                        }
2034
                        return new ChipLocation(x, y);
2✔
2035
                }
2036

2037
                @Override
2038
                public String getMachine() {
2039
                        return machineName;
2✔
2040
                }
2041

2042
                @Override
2043
                public BoardCoordinates getLogical() {
2044
                        return logical;
2✔
2045
                }
2046

2047
                @Override
2048
                public BoardPhysicalCoordinates getPhysical() {
2049
                        return physical;
2✔
2050
                }
2051

2052
                @Override
2053
                public ChipLocation getChip() {
2054
                        return chip;
2✔
2055
                }
2056

2057
                @Override
2058
                public Job getJob() {
2059
                        return job;
2✔
2060
                }
2061
        }
2062

2063
        static class PartialJobException extends IllegalStateException {
2064
                private static final long serialVersionUID = 2997856394666135483L;
2065

2066
                PartialJobException() {
2067
                        super("partial job only");
×
2068
                }
×
2069
        }
2070
}
2071

2072
class ReportRollbackExn extends RuntimeException {
2073
        private static final long serialVersionUID = 1L;
2074

2075
        @FormatMethod
2076
        ReportRollbackExn(String msg, Object... args) {
2077
                super(format(msg, args));
×
2078
        }
×
2079

2080
        ReportRollbackExn(HasChipLocation chip) {
2081
                this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
2082
                                chip.getY());
×
2083
        }
×
2084
}
2085

2086
abstract class GroupsException extends RuntimeException {
2087
        private static final long serialVersionUID = 6607077117924279611L;
2088

2089
        GroupsException(String message) {
2090
                super(message);
×
2091
        }
×
2092

2093
        GroupsException(String message, Throwable cause) {
2094
                super(message, cause);
×
2095
        }
×
2096
}
2097

2098
class NoSuchGroupException extends GroupsException {
2099
        private static final long serialVersionUID = 5193818294198205503L;
2100

2101
        @FormatMethod
2102
        NoSuchGroupException(String msg, Object... args) {
2103
                super(format(msg, args));
×
2104
        }
×
2105
}
2106

2107
class MultipleGroupsException extends GroupsException {
2108
        private static final long serialVersionUID = 6284332340565334236L;
2109

2110
        @FormatMethod
2111
        MultipleGroupsException(String msg, Object... args) {
2112
                super(format(msg, args));
×
2113
        }
×
2114
}
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