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

SpiNNakerManchester / JavaSpiNNaker / 6989

29 Aug 2025 03:04PM UTC coverage: 36.245% (-0.06%) from 36.306%
6989

push

github

rowleya
Use the default group for non-local users

1907 of 5890 branches covered (32.38%)

Branch coverage included in aggregate %.

3 of 4 new or added lines in 1 file covered. (75.0%)

26 existing lines in 5 files now uncovered.

8956 of 24081 relevant lines covered (37.19%)

0.74 hits per line

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

65.81
/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 -> createJobInGroup(
2✔
547
                                owner, getOnlyGroup(conn, owner), descriptor, machineName,
2✔
548
                                tags, keepaliveInterval, originalRequest));
549
        }
550

551
        @Override
552
        public Job createJobInCollabSession(String owner,
553
                        String nmpiCollab, CreateDescriptor descriptor,
554
                        String machineName, List<String> tags, Duration keepaliveInterval,
555
                        byte[] originalRequest) {
556
                var session = quotaManager.createSession(nmpiCollab, owner);
×
557
                var quotaUnits = session.getResourceUsage().getUnits();
×
558

559
                // Use the Collab name as the group, as it should exist
560
                var job = execute(conn -> createJobInGroup(
×
561
                                owner, nmpiCollab, descriptor, machineName,
562
                                tags, keepaliveInterval, originalRequest));
563

564
                quotaManager.associateNMPISession(job.getId(), session.getId(),
×
565
                                quotaUnits);
566

567
                // Return the job created
568
                return job;
×
569
        }
570

571
        @Override
572
        public Job createJobForNMPIJob(String owner, int nmpiJobId,
573
                        CreateDescriptor descriptor, String machineName, List<String> tags,
574
                        Duration keepaliveInterval,        byte[] originalRequest) {
575
                var collab = quotaManager.mayUseNMPIJob(owner, nmpiJobId);
×
576
                if (collab.isEmpty()) {
×
577
                        throw new IllegalArgumentException("User cannot create session in "
×
578
                                        + "NMPI job" + nmpiJobId);
579
                }
580
                var quotaDetails = collab.get();
×
581

582
                var job = execute(conn -> createJobInGroup(
×
583
                                owner, quotaDetails.collabId, descriptor, machineName,
584
                                tags, keepaliveInterval, originalRequest));
585

586
                quotaManager.associateNMPIJob(job.getId(), nmpiJobId,
×
587
                                quotaDetails.quotaUnits);
588

589
                // Return the job created
590
                return job;
×
591
        }
592

593
        /**
594
         * Get the specified group.
595
         *
596
         * @param conn
597
         *            DB connection
598
         * @param user
599
         *            Who is the user?
600
         * @param groupName
601
         *            What group did they specify? (May be {@code null} to say "pick
602
         *            the unique valid possibility for the owner".)
603
         * @return The group ID.
604
         * @throws GroupsException
605
         *             If we can't get a definite group to account against.
606
         */
607
        private int selectGroup(Connection conn, String user, String groupName) {
608
                try (var getGroup = conn.query(GET_GROUP_BY_NAME_AND_MEMBER)) {
2✔
609
                        return getGroup.call1(integer("group_id"), user, groupName)
2✔
610
                                        .orElseThrow(() -> new NoSuchGroupException(
2✔
611
                                                        "group %s does not exist or %s "
612
                                                                        + "is not a member of it",
613
                                                        groupName, user));
614
                }
615
        }
616

617
        private String getOnlyGroup(Connection conn, String user) {
618
                var isInternal = conn.query(GET_USER_DETAILS).call1(
2✔
619
                                (row) -> row.getBoolean("is_internal"), user).orElse(true);
2✔
620

621
                // OIDC users can use a private group
622
                if (!isInternal) {
2!
NEW
623
                        return PRIVATE_COLLAB_PREFIX + user;
×
624
                }
625

626
                try (var listGroups = conn.query(GET_GROUP_NAMES_OF_USER)) {
2✔
627
                        // No name given; need to guess.
628
                        var groups = listGroups.call(row -> row.getString("group_name"),
2✔
629
                                        user);
630
                        if (groups.size() > 1) {
2!
631
                                throw new NoSuchGroupException(
×
632
                                                "User is a member of more than one group, so the group"
633
                                                + " must be selected in the request");
634
                        }
635
                        if (groups.size() == 0) {
2!
636
                                throw new NoSuchGroupException(
×
637
                                                "User is not a member of any group!");
638
                        }
639
                        return groups.get(0);
2✔
640
                }
641
        }
642

643
        private static Optional<Integer> getUser(Connection conn, String userName) {
644
                try (var getUser = conn.query(GET_USER_ID)) {
2✔
645
                        return getUser.call1(integer("user_id"), userName);
2✔
646
                }
647
        }
648

649
        private class BoardLocated {
650
                private int boardId;
651

652
                private int z;
653

654
                BoardLocated(Row row) {
2✔
655
                        boardId = row.getInt("board_id");
2✔
656
                        z = row.getInt("z");
2✔
657
                }
2✔
658
        }
659

660
        /**
661
         * Resolve a machine name and {@link HasBoardCoords} to a board identifier.
662
         *
663
         * @param conn
664
         *            How to get to the DB.
665
         * @param machineName
666
         *            The name of the machine.
667
         * @param b
668
         *            The request that is the coordinate holder.
669
         * @param requireTriadRoot
670
         *            Whether we require the Z coordinate to be zero.
671
         * @return The board ID.
672
         * @throws IllegalArgumentException
673
         *             If the board doesn't exist or it is a board that is not a
674
         *             root of a triad when a triad root is required.
675
         */
676
        private Integer locateBoard(Connection conn, String machineName,
677
                        HasBoardCoords b, boolean requireTriadRoot) {
678
                try (var findTriad = conn.query(FIND_BOARD_BY_NAME_AND_XYZ);
2✔
679
                                var findPhysical = conn.query(FIND_BOARD_BY_NAME_AND_CFB);
2✔
680
                                var findIP = conn.query(FIND_BOARD_BY_NAME_AND_IP_ADDRESS)) {
2✔
681
                        if (nonNull(b.triad)) {
2✔
682
                                return findTriad.call1(BoardLocated::new,
2✔
683
                                                machineName, b.triad.x, b.triad.y, b.triad.z)
2✔
684
                                                .filter(board -> !requireTriadRoot || board.z == 0)
2!
685
                                                .map(board -> board.boardId)
2✔
686
                                                .orElseThrow(() -> new IllegalArgumentException(
2✔
687
                                                                NO_BOARD_MSG));
688
                        } else if (nonNull(b.physical)) {
2!
689
                                return findPhysical.call1(
×
690
                                                BoardLocated::new, machineName, b.physical.c,
×
691
                                                                b.physical.f, b.physical.b)
×
692
                                                .filter(board -> !requireTriadRoot || board.z == 0)
×
693
                                                .map(board -> board.boardId)
×
694
                                                .orElseThrow(() -> new IllegalArgumentException(
×
695
                                                                NO_BOARD_MSG));
696
                        } else {
697
                                return findIP.call1(BoardLocated::new, machineName, b.ip)
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
                        }
703
                }
2!
704
        }
705

706
        private static Optional<Integer> insertJob(Connection conn, MachineImpl m,
707
                        int owner, int group, Duration keepaliveInterval, byte[] req) {
708
                try (var makeJob = conn.update(INSERT_JOB)) {
2✔
709
                        return makeJob.key(m.id, owner, group, keepaliveInterval, req);
2✔
710
                }
711
        }
712

713
        private Optional<MachineImpl> selectMachine(Connection conn,
714
                        CreateDescriptor descriptor, String machineName,
715
                        List<String> tags) {
716
                if (nonNull(machineName)) {
2!
717
                        var m = getMachine(machineName, false, conn);
2✔
718
                        if (m.isPresent() && isAllocPossible(conn, descriptor, m.get())) {
2!
719
                                return m;
2✔
720
                        }
721
                        return Optional.empty();
×
722
                }
723

724
                if (!tags.isEmpty()) {
×
725
                        for (var m : getMachines(conn, false).values()) {
×
726
                                var mi = (MachineImpl) m;
×
727
                                if (mi.tags.containsAll(tags)
×
728
                                                && isAllocPossible(conn, descriptor, mi)) {
×
729
                                        return Optional.of(mi);
×
730
                                }
731
                        }
×
732
                }
733
                return Optional.empty();
×
734
        }
735

736
        private boolean isAllocPossible(final Connection conn,
737
                        final CreateDescriptor descriptor,
738
                        final MachineImpl m) {
739
                return descriptor.visit(new CreateVisitor<Boolean>() {
2✔
740
                        @Override
741
                        public Boolean numBoards(CreateNumBoards nb) {
742
                                try (var getNBoards = conn.query(COUNT_FUNCTIONING_BOARDS)) {
2✔
743
                                        var numBoards = getNBoards.call1(integer("c"), m.id)
2✔
744
                                                        .orElseThrow();
2✔
745
                                        return numBoards >= nb.numBoards;
2!
746
                                }
747
                        }
748

749
                        @Override
750
                        public Boolean dimensions(CreateDimensions d) {
751
                                try (var checkPossible = conn.query(checkRectangle)) {
2✔
752
                                        return checkPossible.call1((r) -> true, d.width, d.height,
2✔
753
                                                        m.id, d.maxDead).isPresent();
2✔
754
                                }
755
                        }
756

757
                        @Override
758
                        public Boolean dimensionsAt(CreateDimensionsAt da) {
759
                                try (var checkPossible = conn.query(checkRectangleAt)) {
2✔
760
                                        int board = locateBoard(conn, m.name, da, true);
2✔
761
                                        return checkPossible.call1((r) -> true, board,
2✔
762
                                                        da.width, da.height, m.id, da.maxDead).isPresent();
2✔
763
                                } catch (IllegalArgumentException e) {
×
764
                                        // This means the board doesn't exist on the given machine
765
                                        return false;
×
766
                                }
767
                        }
768

769
                        @Override
770
                        public Boolean board(CreateBoard b) {
771
                                try (var check = conn.query(CHECK_LOCATION)) {
2✔
772
                                        int board = locateBoard(conn, m.name, b, false);
2✔
773
                                        return check.call1((r) -> true, m.id, board).isPresent();
2✔
774
                                } catch (IllegalArgumentException e) {
×
775
                                        // This means the board doesn't exist on the given machine
776
                                        return false;
×
777
                                }
778
                        }
779
                });
780
        }
781

782
        @Override
783
        public void purgeDownCache() {
784
                synchronized (this) {
×
785
                        downBoardsCache.clear();
×
786
                        downLinksCache.clear();
×
787
                }
×
788
        }
×
789

790
        private static String mergeDescription(HasChipLocation coreLocation,
791
                        String description) {
792
                if (isNull(description)) {
2!
793
                        description = "<null>";
×
794
                }
795
                if (coreLocation instanceof HasCoreLocation) {
2!
796
                        var loc = (HasCoreLocation) coreLocation;
×
797
                        description += format(" (at core %d of chip %s)", loc.getP(),
×
798
                                        loc.asChipLocation());
×
799
                } else if (nonNull(coreLocation)) {
2!
800
                        description +=
×
801
                                        format(" (at chip %s)", coreLocation.asChipLocation());
×
802
                }
803
                return description;
2✔
804
        }
805

806
        private class Problem {
807
                private int boardId;
808

809
                private Integer jobId;
810

811
                Problem(Row row) {
2✔
812
                        boardId = row.getInt("board_id");
2✔
813
                        jobId = row.getInt("job_id");
2✔
814
                }
2✔
815
        }
816

817
        @Override
818
        public void reportProblem(String address, HasChipLocation coreLocation,
819
                        String description, Permit permit) {
820
                try (var sql = new BoardReportSQL()) {
2✔
821
                        var desc = mergeDescription(coreLocation, description);
2✔
822
                        var email = sql.transaction(() -> {
2✔
823
                                var machines = getMachines(sql.getConnection(), true).values();
2✔
824
                                for (var m : machines) {
2✔
825
                                        var mail = sql.findBoardNet.call1(
2✔
826
                                                        Problem::new, m.getId(), address)
2✔
827
                                                        .flatMap(prob -> reportProblem(prob, desc, permit,
2✔
828
                                                                        sql));
829
                                        if (mail.isPresent()) {
2!
830
                                                return mail;
×
831
                                        }
832
                                }
2✔
833
                                return Optional.empty();
2✔
834
                        });
835
                        // Outside the transaction!
836
                        email.ifPresent(emailSender::sendServiceMail);
2✔
837
                } catch (ReportRollbackExn e) {
×
838
                        log.warn("failed to handle problem report", e);
×
839
                }
2✔
840
        }
2✔
841

842
        private Optional<EmailBuilder> reportProblem(Problem problem,
843
                        String description,        Permit permit, BoardReportSQL sql) {
844
                var email = new EmailBuilder(problem.jobId);
2✔
845
                email.header(description, 1, permit.name);
2✔
846
                int userId = getUser(sql.getConnection(), permit.name).orElseThrow(
2✔
847
                                () -> new ReportRollbackExn("no such user: %s", permit.name));
×
848
                sql.insertReport.key(problem.boardId, problem.jobId,
2✔
849
                                description, userId).ifPresent(email::issue);
2✔
850
                return takeBoardsOutOfService(sql, email).map(acted -> {
2✔
851
                        email.footer(acted);
×
852
                        return email;
×
853
                });
854
        }
855

856
        private class Reported {
857
                private int boardId;
858

859
                private int x;
860

861
                private int y;
862

863
                private int z;
864

865
                private String address;
866

867
                private int numReports;
868

869
                Reported(Row row) {
×
870
                        boardId = row.getInt("board_id");
×
871
                        x = row.getInt("x");
×
872
                        y = row.getInt("y");
×
873
                        z = row.getInt("z");
×
874
                        address = row.getString("address");
×
875
                        numReports = row.getInt("numReports");
×
876
                }
×
877

878
        }
879

880
        /**
881
         * Take boards out of service if they've been reported frequently enough.
882
         *
883
         * @param sql
884
         *            How to touch the DB
885
         * @param email
886
         *            The email we are building.
887
         * @return The number of boards taken out of service
888
         */
889
        private Optional<Integer> takeBoardsOutOfService(BoardReportSQL sql,
890
                        EmailBuilder email) {
891
                int acted = 0;
2✔
892
                for (var report : sql.getReported.call(Reported::new,
2!
893
                                props.getReportActionThreshold())) {
2✔
894
                        if (sql.setFunctioning.call(false, report.boardId) > 0) {
×
895
                                email.serviceActionDone(report);
×
896
                                acted++;
×
897
                        }
898
                }
×
899
                if (acted > 0) {
2!
900
                        purgeDownCache();
×
901
                }
902
                return acted > 0 ? Optional.of(acted) : Optional.empty();
2!
903
        }
904

905
        private static DownLink makeDownLinkFromRow(Row row) {
906
                // Non-standard column names to reduce number of queries
907
                var board1 = new BoardCoords(row.getInt("board_1_x"),
×
908
                                row.getInt("board_1_y"), row.getInt("board_1_z"),
×
909
                                row.getInt("board_1_c"), row.getInt("board_1_f"),
×
910
                                row.getInteger("board_1_b"), row.getString("board_1_addr"));
×
911
                var board2 = new BoardCoords(row.getInt("board_2_x"),
×
912
                                row.getInt("board_2_y"), row.getInt("board_2_z"),
×
913
                                row.getInt("board_2_c"), row.getInt("board_2_f"),
×
914
                                row.getInteger("board_2_b"), row.getString("board_2_addr"));
×
915
                return new DownLink(board1, row.getEnum("dir_1", Direction.class),
×
916
                                board2, row.getEnum("dir_2", Direction.class));
×
917
        }
918

919
        public void emergencyStop(String commandCode) {
920
                if (!commandCode.equals(props.getEmergencyStopCommandCode())) {
×
921
                        throw new IllegalArgumentException("Invalid emergency stop code");
×
922
                }
923
                allocator.emergencyStop();
×
924
                emergencyStop = true;
×
925
                log.warn("Emergency stop requested!");
×
926
        }
×
927

928
        private class MachineImpl implements Machine {
929
                private final int id;
930

931
                private final boolean inService;
932

933
                private final String name;
934

935
                private final Set<String> tags;
936

937
                private final int width;
938

939
                private final int height;
940

941
                private boolean lookedUpWraps;
942

943
                private boolean hWrap;
944

945
                private boolean vWrap;
946

947
                @JsonIgnore
948
                private final Epoch epoch;
949

950
                MachineImpl(Connection conn, Row rs) {
2✔
951
                        id = rs.getInt("machine_id");
2✔
952
                        name = rs.getString("machine_name");
2✔
953
                        width = rs.getInt("width");
2✔
954
                        height = rs.getInt("height");
2✔
955
                        inService = rs.getBoolean("in_service");
2✔
956
                        lookedUpWraps = false;
2✔
957
                        try (var getTags = conn.query(GET_TAGS)) {
2✔
958
                                tags = Row.stream(copy(getTags.call(string("tag"), id)))
2✔
959
                                                .toSet();
2✔
960
                        }
961

962
                        this.epoch = epochs.getMachineEpoch(id);
2✔
963
                }
2✔
964

965
                private int getArea() {
966
                        return width * height * TRIAD_DEPTH;
2✔
967
                }
968

969
                @Override
970
                public boolean waitForChange(Duration timeout) {
971
                        if (isNull(epoch)) {
2!
972
                                log.info("Machine {} epoch is null!", id);
×
973
                                return true;
×
974
                        }
975
                        try {
976
                                log.info("Waiting for change in epoch for {}", id);
2✔
977
                                return epoch.waitForChange(timeout);
×
978
                        } catch (InterruptedException interrupted) {
2✔
979
                                log.info("Interrupted waiting for change on {}", id);
2✔
980
                                return false;
2✔
981
                        }
982
                }
983

984
                @Override
985
                public Optional<BoardLocation> getBoardByChip(HasChipLocation chip) {
986
                        try (var conn = getConnection();
2✔
987
                                        var findBoard = conn.query(findBoardByGlobalChip)) {
2✔
988
                                return conn.transaction(false,
2✔
989
                                                () -> findBoard.call1(
2✔
990
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
991
                                                                chip.getX(), chip.getY()));
2✔
992
                        }
993
                }
994

995
                @Override
996
                public Optional<BoardLocation> getBoardByPhysicalCoords(
997
                                PhysicalCoords coords) {
998
                        try (var conn = getConnection();
2✔
999
                                        var findBoard = conn.query(findBoardByPhysicalCoords)) {
2✔
1000
                                return conn.transaction(false,
2✔
1001
                                                () -> findBoard.call1(
2✔
1002
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1003
                                                                coords.c, coords.f, coords.b));
2✔
1004
                        }
1005
                }
1006

1007
                @Override
1008
                public Optional<BoardLocation> getBoardByLogicalCoords(
1009
                                TriadCoords coords) {
1010
                        try (var conn = getConnection();
2✔
1011
                                        var findBoard = conn.query(findBoardByLogicalCoords)) {
2✔
1012
                                return conn.transaction(false,
2✔
1013
                                                () -> findBoard.call1(
2✔
1014
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1015
                                                                coords.x, coords.y, coords.z));
2✔
1016
                        }
1017
                }
1018

1019
                @Override
1020
                public Optional<BoardLocation> getBoardByIPAddress(String address) {
1021
                        try (var conn = getConnection();
2✔
1022
                                        var findBoard = conn.query(findBoardByIPAddress)) {
2✔
1023
                                return conn.transaction(false,
2✔
1024
                                                () -> findBoard.call1(
2✔
1025
                                                                row -> new BoardLocationImpl(row, this), id,
2✔
1026
                                                                address));
1027
                        }
1028
                }
1029

1030
                @Override
1031
                public String getRootBoardBMPAddress() {
1032
                        try (var conn = getConnection();
2✔
1033
                                        var rootBMPaddr = conn.query(GET_ROOT_BMP_ADDRESS)) {
2✔
1034
                                return conn.transaction(false, () -> rootBMPaddr.call1(
2✔
1035
                                                string("address"), id).orElse(null));
2✔
1036
                        }
1037
                }
1038

1039
                @Override
1040
                public List<Integer> getBoardNumbers() {
1041
                        try (var conn = getConnection();
2✔
1042
                                        var boardNumbers = conn.query(GET_BOARD_NUMBERS)) {
2✔
1043
                                return conn.transaction(false, () -> boardNumbers.call(
2✔
1044
                                                integer("board_num"), id));
2✔
1045
                        }
1046
                }
1047

1048
                @Override
1049
                public List<BoardCoords> getDeadBoards() {
1050
                        // Assume that the list doesn't change for the duration of this obj
1051
                        synchronized (Spalloc.this) {
2✔
1052
                                var down = downBoardsCache.get(name);
2✔
1053
                                if (nonNull(down)) {
2✔
1054
                                        return copy(down);
2✔
1055
                                }
1056
                        }
2✔
1057
                        try (var conn = getConnection();
2✔
1058
                                        var boardNumbers = conn.query(GET_DEAD_BOARDS)) {
2✔
1059
                                var downBoards = conn.transaction(false,
2✔
1060
                                                () -> boardNumbers.call(
2✔
1061
                                                                row -> new BoardCoords(row, false), id));
2✔
1062
                                synchronized (Spalloc.this) {
2✔
1063
                                        downBoardsCache.putIfAbsent(name, downBoards);
2✔
1064
                                }
2✔
1065
                                return copy(downBoards);
2✔
1066
                        }
1067
                }
1068

1069
                @Override
1070
                public List<DownLink> getDownLinks() {
1071
                        // Assume that the list doesn't change for the duration of this obj
1072
                        synchronized (Spalloc.this) {
2✔
1073
                                var down = downLinksCache.get(name);
2✔
1074
                                if (nonNull(down)) {
2✔
1075
                                        return copy(down);
2✔
1076
                                }
1077
                        }
2✔
1078
                        try (var conn = getConnection();
2✔
1079
                                        var boardNumbers = conn.query(getDeadLinks)) {
2✔
1080
                                var downLinks = conn.transaction(false, () -> boardNumbers
2✔
1081
                                                .call(Spalloc::makeDownLinkFromRow, id));
2✔
1082
                                synchronized (Spalloc.this) {
2✔
1083
                                        downLinksCache.putIfAbsent(name, downLinks);
2✔
1084
                                }
2✔
1085
                                return copy(downLinks);
2✔
1086
                        }
1087
                }
1088

1089
                @Override
1090
                public List<Integer> getAvailableBoards() {
1091
                        try (var conn = getConnection();
2✔
1092
                                        var boardNumbers = conn
2✔
1093
                                                        .query(GET_AVAILABLE_BOARD_NUMBERS)) {
2✔
1094
                                return conn.transaction(false, () -> boardNumbers.call(
2✔
1095
                                                integer("board_num"), id));
2✔
1096
                        }
1097
                }
1098

1099
                @Override
1100
                public int getId() {
1101
                        return id;
2✔
1102
                }
1103

1104
                @Override
1105
                public String getName() {
1106
                        return name;
2✔
1107
                }
1108

1109
                @Override
1110
                public Set<String> getTags() {
1111
                        return tags;
2✔
1112
                }
1113

1114
                @Override
1115
                public int getWidth() {
1116
                        return width;
2✔
1117
                }
1118

1119
                @Override
1120
                public int getHeight() {
1121
                        return height;
2✔
1122
                }
1123

1124
                @Override
1125
                public boolean isInService() {
1126
                        return inService;
2✔
1127
                }
1128

1129
                @Override
1130
                public String getBMPAddress(BMPCoords bmp) {
1131
                        try (var conn = getConnection();
2✔
1132
                                        var bmpAddr = conn.query(GET_BMP_ADDRESS)) {
2✔
1133
                                return conn.transaction(false,
2✔
1134
                                                () -> bmpAddr
1135
                                                                .call1(string("address"), id, bmp.getCabinet(),
2✔
1136
                                                                                bmp.getFrame()).orElse(null));
2✔
1137
                        }
1138
                }
1139

1140
                @Override
1141
                public List<Integer> getBoardNumbers(BMPCoords bmp) {
1142
                        try (var conn = getConnection();
2✔
1143
                                        var boardNumbers = conn.query(GET_BMP_BOARD_NUMBERS)) {
2✔
1144
                                return conn.transaction(false,
2✔
1145
                                                () -> boardNumbers
2✔
1146
                                                                .call(integer("board_num"), id,
2✔
1147
                                                                                bmp.getCabinet(), bmp.getFrame()));
2✔
1148
                        }
1149
                }
1150

1151
                @Override
1152
                public boolean equals(Object other) {
1153
                        // Equality is defined exactly by the database ID
1154
                        return (other instanceof MachineImpl)
×
1155
                                        && (id == ((MachineImpl) other).id);
1156
                }
1157

1158
                @Override
1159
                public int hashCode() {
1160
                        return id;
×
1161
                }
1162

1163
                @Override
1164
                public String toString() {
1165
                        return "Machine(" + name + ")";
×
1166
                }
1167

1168
                private void retrieveWraps() {
1169
                        try (var conn = getConnection();
×
1170
                                        var getWraps = conn.query(GET_MACHINE_WRAPS)) {
×
1171
                                /*
1172
                                 * No locking; not too bothered which thread asks as result will
1173
                                 * be the same either way
1174
                                 */
1175
                                lookedUpWraps =
×
1176
                                                conn.transaction(false, () -> getWraps.call1(rs -> {
×
1177
                                                        hWrap = rs.getBoolean("horizontal_wrap");
×
1178
                                                        vWrap = rs.getBoolean("vertical_wrap");
×
1179
                                                        return true;
×
1180
                                                }, id)).orElse(false);
×
1181
                        }
1182
                }
×
1183

1184
                @Override
1185
                public boolean isHorizonallyWrapped() {
1186
                        if (!lookedUpWraps) {
×
1187
                                retrieveWraps();
×
1188
                        }
1189
                        return hWrap;
×
1190
                }
1191

1192
                @Override
1193
                public boolean isVerticallyWrapped() {
1194
                        if (!lookedUpWraps) {
×
1195
                                retrieveWraps();
×
1196
                        }
1197
                        return vWrap;
×
1198
                }
1199
        }
1200

1201
        private final class JobCollection implements Jobs {
1202
                @JsonIgnore
1203
                private final Epoch epoch;
1204

1205
                private final List<Job> jobs;
1206

1207
                private JobCollection(List<Job> jobs) {
2✔
1208
                        this.jobs = jobs;
2✔
1209
                        if (jobs.isEmpty()) {
2✔
1210
                                epoch = null;
2✔
1211
                        } else {
1212
                                epoch = epochs.getJobsEpoch(
2✔
1213
                                                jobs.stream().map(Job::getId).collect(toList()));
2✔
1214
                        }
1215
                }
2✔
1216

1217
                @Override
1218
                public boolean waitForChange(Duration timeout) {
1219
                        if (isNull(epoch)) {
×
1220
                                return true;
×
1221
                        }
1222
                        try {
1223
                                return epoch.waitForChange(timeout);
×
1224
                        } catch (InterruptedException interrupted) {
×
1225
                                currentThread().interrupt();
×
1226
                                return false;
×
1227
                        }
1228
                }
1229

1230
                /**
1231
                 * Get the set of jobs changed.
1232
                 *
1233
                 * @param timeout
1234
                 *            The timeout to wait for until something happens.
1235
                 * @return The set of changed job identifiers.
1236
                 */
1237
                @Override
1238
                public Collection<Integer> getChanged(Duration timeout) {
1239
                        if (isNull(epoch)) {
2!
1240
                                return jobs.stream().map(Job::getId).collect(toSet());
2✔
1241
                        }
1242
                        try {
1243
                                return epoch.getChanged(timeout);
×
1244
                        } catch (InterruptedException interrupted) {
×
1245
                                currentThread().interrupt();
×
1246
                                return jobs.stream().map(Job::getId).collect(toSet());
×
1247
                        }
1248
                }
1249

1250
                @Override
1251
                public List<Job> jobs() {
1252
                        return copy(jobs);
×
1253
                }
1254

1255
                @Override
1256
                public List<Integer> ids() {
1257
                        return jobs.stream().map(Job::getId).collect(toList());
2✔
1258
                }
1259
        }
1260

1261
        private final class BoardReportSQL extends AbstractSQL {
2✔
1262
                private final Query findBoardByChip = conn.query(findBoardByJobChip);
2✔
1263

1264
                private final Query findBoardByTriad = conn.query(
2✔
1265
                                findBoardByLogicalCoords);
2✔
1266

1267
                private final Query findBoardPhys = conn.query(
2✔
1268
                                findBoardByPhysicalCoords);
2✔
1269

1270
                private final Query findBoardNet = conn.query(findBoardByIPAddress);
2✔
1271

1272
                private final Update insertReport = conn.update(INSERT_BOARD_REPORT);
2✔
1273

1274
                private final Query getReported = conn.query(getReportedBoards);
2✔
1275

1276
                private final Update setFunctioning = conn.update(
2✔
1277
                                SET_FUNCTIONING_FIELD);
1278

1279
                private final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
2✔
1280

1281
                @Override
1282
                public void close() {
1283
                        findBoardByChip.close();
2✔
1284
                        findBoardByTriad.close();
2✔
1285
                        findBoardPhys.close();
2✔
1286
                        findBoardNet.close();
2✔
1287
                        insertReport.close();
2✔
1288
                        getReported.close();
2✔
1289
                        setFunctioning.close();
2✔
1290
                        getNamedMachine.close();
2✔
1291
                        super.close();
2✔
1292
                }
2✔
1293
        }
1294

1295
        /** Used to assemble an issue-report email for sending. */
1296
        private static final class EmailBuilder {
1297
                /**
1298
                 * More efficient than several String.format() calls, and much clearer
1299
                 * than a mess of direct {@link StringBuilder} calls!
1300
                 */
1301
                private final Formatter b = new Formatter(Locale.UK);
2✔
1302

1303
                private final int id;
1304

1305
                /**
1306
                 * @param id
1307
                 *            The job ID
1308
                 */
1309
                EmailBuilder(int id) {
2✔
1310
                        this.id = id;
2✔
1311
                }
2✔
1312

1313
                void header(String issue, int numBoards, String who) {
1314
                        b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
2✔
1315
                                        numBoards, who);
2✔
1316
                }
2✔
1317

1318
                void chip(ReportedBoard board) {
1319
                        b.format("\tBoard for job (%d) chip %s\n", //
×
1320
                                        id, board.chip);
×
1321
                }
×
1322

1323
                void triad(ReportedBoard board) {
1324
                        b.format("\tBoard for job (%d) board (X:%d,Y:%d,Z:%d)\n", //
×
1325
                                        id, board.x, board.y, board.z);
×
1326
                }
×
1327

1328
                void phys(ReportedBoard board) {
1329
                        b.format(
×
1330
                                        "\tBoard for job (%d) board "
1331
                                                        + "[Cabinet:%d,Frame:%d,Board:%d]\n", //
1332
                                        id, board.cabinet, board.frame, board.board);
×
1333
                }
×
1334

1335
                void ip(ReportedBoard board) {
1336
                        b.format("\tBoard for job (%d) board (IP: %s)\n", //
2✔
1337
                                        id, board.address);
2✔
1338
                }
2✔
1339

1340
                void issue(int issueId) {
1341
                        b.format("\t\tAction: noted as issue #%d\n", //
2✔
1342
                                        issueId);
2✔
1343
                }
2✔
1344

1345
                void footer(int numActions) {
1346
                        b.format("\nSummary: %d boards taken out of service.\n",
×
1347
                                        numActions);
×
1348
                }
×
1349

1350
                void serviceActionDone(Reported report) {
1351
                        b.format(
×
1352
                                        "\tAction: board (X:%d,Y:%d,Z:%d) (IP: %s) "
1353
                                                        + "taken out of service once not in use "
1354
                                                        + "(%d problems reported)\n",
1355
                                        report.x, report.y, report.z,
×
1356
                                        report.address, report.numReports);
×
1357
                }
×
1358

1359
                /** @return The assembled message body. */
1360
                @Override
1361
                public String toString() {
1362
                        return b.toString();
×
1363
                }
1364
        }
1365

1366
        private final class JobImpl implements Job {
1367
                @JsonIgnore
1368
                private Epoch epoch;
1369

1370
                private final int id;
1371

1372
                private final int machineId;
1373

1374
                private Integer width;
1375

1376
                private Integer height;
1377

1378
                private Integer depth;
1379

1380
                private JobState state;
1381

1382
                /** If not {@code null}, the ID of the root board of the job. */
1383
                private Integer root;
1384

1385
                private ChipLocation chipRoot;
1386

1387
                private String owner;
1388

1389
                private String keepaliveHost;
1390

1391
                private Instant startTime;
1392

1393
                private Instant keepaliveTime;
1394

1395
                private Instant finishTime;
1396

1397
                private String deathReason;
1398

1399
                private byte[] request;
1400

1401
                private boolean partial;
1402

1403
                private MachineImpl cachedMachine;
1404

1405
                JobImpl(int id, int machineId) {
2✔
1406
                        this.epoch = epochs.getJobsEpoch(id);
2✔
1407
                        this.id = id;
2✔
1408
                        this.machineId = machineId;
2✔
1409
                        partial = true;
2✔
1410
                }
2✔
1411

1412
                JobImpl(int jobId, int machineId, JobState jobState,
1413
                                Instant keepalive) {
1414
                        this(jobId, machineId);
2✔
1415
                        state = jobState;
2✔
1416
                        keepaliveTime = keepalive;
2✔
1417
                }
2✔
1418

1419
                JobImpl(Connection conn, Row row) {
2✔
1420
                        this.id = row.getInt("job_id");
2✔
1421
                        this.machineId = row.getInt("machine_id");
2✔
1422
                        width = row.getInteger("width");
2✔
1423
                        height = row.getInteger("height");
2✔
1424
                        depth = row.getInteger("depth");
2✔
1425
                        root = row.getInteger("root_id");
2✔
1426
                        owner = row.getString("owner");
2✔
1427
                        if (nonNull(root)) {
2✔
1428
                                try (var boardRoot = conn.query(GET_ROOT_OF_BOARD)) {
2✔
1429
                                        chipRoot = boardRoot.call1(chip("root_x", "root_y"), root)
2✔
1430
                                                        .orElse(null);
2✔
1431
                                }
1432
                        }
1433
                        state = row.getEnum("job_state", JobState.class);
2✔
1434
                        keepaliveHost = row.getString("keepalive_host");
2✔
1435
                        keepaliveTime = row.getInstant("keepalive_timestamp");
2✔
1436
                        startTime = row.getInstant("create_timestamp");
2✔
1437
                        finishTime = row.getInstant("death_timestamp");
2✔
1438
                        deathReason = row.getString("death_reason");
2✔
1439
                        request = row.getBytes("original_request");
2✔
1440
                        partial = false;
2✔
1441

1442
                        this.epoch = epochs.getJobsEpoch(id);
2✔
1443
                }
2✔
1444

1445
                /**
1446
                 * Get the machine that this job is running on. May used a cached value.
1447
                 * A transaction is required, but may be a read-only transaction.
1448
                 *
1449
                 * @param conn
1450
                 *            The connection to the DB
1451
                 * @return The overall machine handle.
1452
                 */
1453
                private synchronized MachineImpl getJobMachine(Connection conn) {
1454
                        if (cachedMachine == null || !cachedMachine.epoch.isValid()) {
2!
1455
                                cachedMachine = Spalloc.this.getMachine(machineId, true, conn)
2✔
1456
                                                .orElseThrow();
2✔
1457
                        }
1458
                        return cachedMachine;
2✔
1459
                }
1460

1461
                @Override
1462
                public void access(String keepaliveAddress) {
1463
                        if (partial) {
2!
1464
                                throw new PartialJobException();
×
1465
                        }
1466
                        try (var conn = getConnection();
2✔
1467
                                        var keepAlive = conn.update(UPDATE_KEEPALIVE)) {
2✔
1468
                                conn.transaction(() -> keepAlive.call(keepaliveAddress, id));
2✔
1469
                        }
1470
                }
2✔
1471

1472
                @Override
1473
                public void destroy(String reason) {
1474
                        if (partial) {
2!
1475
                                throw new PartialJobException();
×
1476
                        }
1477
                        powerController.destroyJob(id, reason);
2✔
1478
                        rememberer.closeJob(id);
2✔
1479
                }
2✔
1480

1481
                @Override
1482
                public void setPower(boolean power) {
1483
                        powerController.setPower(id, power ? ON : OFF, READY);
×
1484
                }
×
1485

1486
                @Override
1487
                public boolean waitForChange(Duration timeout) {
1488
                        if (isNull(epoch)) {
2!
1489
                                return true;
×
1490
                        }
1491
                        try {
1492
                                return epoch.waitForChange(timeout);
×
1493
                        } catch (InterruptedException interrupted) {
2✔
1494
                                currentThread().interrupt();
2✔
1495
                                return false;
2✔
1496
                        }
1497
                }
1498

1499
                @Override
1500
                public int getId() {
1501
                        return id;
2✔
1502
                }
1503

1504
                @Override
1505
                public JobState getState() {
1506
                        return state;
2✔
1507
                }
1508

1509
                @Override
1510
                public Instant getStartTime() {
1511
                        return startTime;
2✔
1512
                }
1513

1514
                @Override
1515
                public Optional<Instant> getFinishTime() {
1516
                        return Optional.ofNullable(finishTime);
2✔
1517
                }
1518

1519
                @Override
1520
                public Optional<String> getReason() {
1521
                        return Optional.ofNullable(deathReason);
2✔
1522
                }
1523

1524
                @Override
1525
                public Optional<String> getKeepaliveHost() {
1526
                        if (partial) {
2!
1527
                                return Optional.empty();
×
1528
                        }
1529
                        return Optional.ofNullable(keepaliveHost);
2✔
1530
                }
1531

1532
                @Override
1533
                public Instant getKeepaliveTimestamp() {
1534
                        return keepaliveTime;
2✔
1535
                }
1536

1537
                @Override
1538
                public Optional<byte[]> getOriginalRequest() {
1539
                        if (partial) {
2!
1540
                                return Optional.empty();
×
1541
                        }
1542
                        return Optional.ofNullable(request);
2✔
1543
                }
1544

1545
                @Override
1546
                public Optional<SubMachine> getMachine() {
1547
                        if (isNull(root)) {
2✔
1548
                                return Optional.empty();
2✔
1549
                        }
1550
                        return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
2✔
1551
                }
1552

1553
                @Override
1554
                public Optional<BoardLocation> whereIs(int x, int y) {
1555
                        if (isNull(root)) {
2!
1556
                                return Optional.empty();
×
1557
                        }
1558
                        try (var conn = getConnection();
2✔
1559
                                        var findBoard = conn.query(findBoardByJobChip)) {
2✔
1560
                                return conn.transaction(false, () -> findBoard
2✔
1561
                                                .call1(row -> new BoardLocationImpl(row,
2✔
1562
                                                                getJobMachine(conn)), id, root, x, y));
2✔
1563
                        }
1564
                }
1565

1566
                // -------------------------------------------------------------
1567
                // Bad board report handling
1568

1569
                @Override
1570
                public String reportIssue(IssueReportRequest report, Permit permit) {
1571
                        try (var q = new BoardReportSQL()) {
2✔
1572
                                var email = new EmailBuilder(id);
2✔
1573
                                var result = q.transaction(
2✔
1574
                                                () -> reportIssue(report, permit, email, q));
2✔
1575
                                emailSender.sendServiceMail(email);
2✔
1576
                                for (var m : report.boards.stream()
2✔
1577
                                                .map(b -> q.getNamedMachine.call1(
2✔
1578
                                                                r -> r.getInt("machine_id"), b.machine, true))
2✔
1579
                                                .collect(toSet())) {
2✔
1580
                                        if (m.isPresent()) {
2!
1581
                                                epochs.machineChanged(m.get());
×
1582
                                        }
1583
                                }
2✔
1584

1585
                                return result;
2✔
1586
                        } catch (ReportRollbackExn e) {
×
1587
                                return e.getMessage();
×
1588
                        }
1589
                }
1590

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

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

1692
                /**
1693
                 * Record a reported issue with a board.
1694
                 *
1695
                 * @param u
1696
                 *            How to touch the DB
1697
                 * @param boardId
1698
                 *            What board has the issue?
1699
                 * @param issue
1700
                 *            What is the issue?
1701
                 * @param userId
1702
                 *            Who is doing the report?
1703
                 * @param email
1704
                 *            The email we are building.
1705
                 */
1706
                private void addIssueReport(BoardReportSQL u, int boardId, String issue,
1707
                                int userId, EmailBuilder email) {
1708
                        u.insertReport.key(boardId, id, issue, userId)
2✔
1709
                                        .ifPresent(email::issue);
2✔
1710
                }
2✔
1711

1712
                // -------------------------------------------------------------
1713

1714
                @Override
1715
                public Optional<ChipLocation> getRootChip() {
1716
                        return Optional.ofNullable(chipRoot);
2✔
1717
                }
1718

1719
                @Override
1720
                public Optional<String> getOwner() {
1721
                        if (partial) {
2!
1722
                                return Optional.empty();
×
1723
                        }
1724
                        return Optional.ofNullable(owner);
2✔
1725
                }
1726

1727
                @Override
1728
                public Optional<Integer> getWidth() {
1729
                        return Optional.ofNullable(width);
2✔
1730
                }
1731

1732
                @Override
1733
                public Optional<Integer> getHeight() {
1734
                        return Optional.ofNullable(height);
2✔
1735
                }
1736

1737
                @Override
1738
                public Optional<Integer> getDepth() {
1739
                        return Optional.ofNullable(depth);
2✔
1740
                }
1741

1742
                @Override
1743
                public void rememberProxy(ProxyCore proxy) {
1744
                        rememberer.rememberProxyForJob(id, proxy);
×
1745
                }
×
1746

1747
                @Override
1748
                public void forgetProxy(ProxyCore proxy) {
1749
                        rememberer.removeProxyForJob(id, proxy);
×
1750
                }
×
1751

1752
                @Override
1753
                @SuppressWarnings("MustBeClosed")
1754
                public TransceiverInterface getTransceiver() throws IOException,
1755
                                InterruptedException, SpinnmanException {
1756
                        var mac = getMachine();
×
1757
                        if (mac.isEmpty()) {
×
1758
                                throw new IllegalStateException(
×
1759
                                                "Job is not active!");
1760
                        }
1761
                        var txrx = rememberer.getTransceiverForJob(id);
×
1762
                        if (nonNull(txrx)) {
×
1763
                                return txrx;
×
1764
                        }
1765
                        List<uk.ac.manchester.spinnaker.connections.model.Connection>
1766
                                connections = new ArrayList<>();
×
1767
                        for (var conn : mac.get().getConnections()) {
×
1768
                                connections.add(new SCPConnection(conn.getChip(),
×
1769
                                                null, null, InetAddress.getByName(conn.getHostname())));
×
1770
                        }
×
1771
                        txrx = new Transceiver(MachineVersion.FIVE, connections);
×
1772
                        var unused = txrx.getMachineDetails();
×
1773
                        rememberer.rememberTransceiverForJob(id, txrx);
×
1774
                        return txrx;
×
1775
                }
1776

1777
                @Override
1778
                @SuppressWarnings("MustBeClosed")
1779
                public FastDataIn getFastDataIn(CoreLocation gathererCore, IPTag iptag)
1780
                                throws ProcessException, IOException, InterruptedException {
1781
                        var fdi = rememberer.getFastDataIn(id, iptag.getDestination());
×
1782
                        if (fdi != null) {
×
1783
                                return fdi;
×
1784
                        }
1785
                        fdi = new FastDataIn(gathererCore, iptag);
×
1786
                        rememberer.rememberFastDataIn(id, iptag.getDestination(), fdi);
×
1787
                        return fdi;
×
1788
                }
1789

1790
                @Override
1791
                @SuppressWarnings("MustBeClosed")
1792
                public Downloader getDownloader(IPTag iptag)
1793
                                throws ProcessException, IOException, InterruptedException {
1794
                        var downloader = rememberer.getDownloader(id,
×
1795
                                        iptag.getDestination());
×
1796
                        if (downloader != null) {
×
1797
                                // Ensure the downloader can be reuse
1798
                                downloader.reuse();
×
1799
                                return downloader;
×
1800
                        }
1801
                        downloader = new Downloader(iptag);
×
1802
                        rememberer.rememberDownloader(id, iptag.getDestination(),
×
1803
                                        downloader);
1804
                        return downloader;
×
1805
                }
1806

1807
                @Override
1808
                public boolean equals(Object other) {
1809
                        // Equality is defined exactly by the database ID
1810
                        return (other instanceof JobImpl) && (id == ((JobImpl) other).id);
×
1811
                }
1812

1813
                @Override
1814
                public int hashCode() {
1815
                        return id;
×
1816
                }
1817

1818
                @Override
1819
                public String toString() {
1820
                        return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1821
                                        width, height, depth, startTime, finishTime);
1822
                }
1823

1824
                private final class SubMachineImpl implements SubMachine {
1825
                        /** The machine that this sub-machine is part of. */
1826
                        private final Machine machine;
1827

1828
                        /** The root X coordinate of this sub-machine. */
1829
                        private int rootX;
1830

1831
                        /** The root Y coordinate of this sub-machine. */
1832
                        private int rootY;
1833

1834
                        /** The root Z coordinate of this sub-machine. */
1835
                        private int rootZ;
1836

1837
                        /** The connection details of this sub-machine. */
1838
                        private List<ConnectionInfo> connections;
1839

1840
                        /** The board locations of this sub-machine. */
1841
                        private List<BoardCoordinates> boards;
1842

1843
                        private List<Integer> boardIds;
1844

1845
                        private SubMachineImpl(Connection conn) {
2✔
1846
                                machine = getJobMachine(conn);
2✔
1847
                                try (var getRootXY = conn.query(GET_ROOT_COORDS);
2✔
1848
                                                var getBoardInfo = conn.query(GET_BOARD_CONNECT_INFO)) {
2✔
1849
                                        getRootXY.call1(row -> {
2✔
1850
                                                rootX = row.getInt("x");
2✔
1851
                                                rootY = row.getInt("y");
2✔
1852
                                                rootZ = row.getInt("z");
2✔
1853
                                                // We have to return something,
1854
                                                // but it doesn't matter what
1855
                                                return true;
2✔
1856
                                        }, root);
1857
                                        int capacityEstimate = width * height;
2✔
1858
                                        connections = new ArrayList<>(capacityEstimate);
2✔
1859
                                        boards = new ArrayList<>(capacityEstimate);
2✔
1860
                                        boardIds = new ArrayList<>(capacityEstimate);
2✔
1861
                                        getBoardInfo.call(row -> {
2✔
1862
                                                boardIds.add(row.getInt("board_id"));
2✔
1863
                                                boards.add(new BoardCoordinates(row.getInt("x"),
2✔
1864
                                                                row.getInt("y"), row.getInt("z")));
2✔
1865
                                                connections.add(new ConnectionInfo(
2✔
1866
                                                                relativeChipLocation(row.getInt("root_x"),
2✔
1867
                                                                                row.getInt("root_y")),
2✔
1868
                                                                row.getString("address")));
2✔
1869
                                                // We have to return something,
1870
                                                // but it doesn't matter what
1871
                                                return true;
2✔
1872
                                        }, id);
2✔
1873
                                }
1874
                        }
2✔
1875

1876
                        private ChipLocation relativeChipLocation(int x, int y) {
1877
                                x -= chipRoot.getX();
2✔
1878
                                y -= chipRoot.getY();
2✔
1879
                                // Allow for wrapping
1880
                                if (x < 0) {
2!
1881
                                        x += machine.getWidth() * TRIAD_CHIP_SIZE;
×
1882
                                }
1883
                                if (y < 0) {
2!
1884
                                        y += machine.getHeight() * TRIAD_CHIP_SIZE;
×
1885
                                }
1886
                                return new ChipLocation(x, y);
2✔
1887
                        }
1888

1889
                        @Override
1890
                        public Machine getMachine() {
1891
                                return machine;
2✔
1892
                        }
1893

1894
                        @Override
1895
                        public int getRootX() {
1896
                                return rootX;
2✔
1897
                        }
1898

1899
                        @Override
1900
                        public int getRootY() {
1901
                                return rootY;
2✔
1902
                        }
1903

1904
                        @Override
1905
                        public int getRootZ() {
1906
                                return rootZ;
2✔
1907
                        }
1908

1909
                        @Override
1910
                        public int getWidth() {
1911
                                return width;
2✔
1912
                        }
1913

1914
                        @Override
1915
                        public int getHeight() {
1916
                                return height;
2✔
1917
                        }
1918

1919
                        @Override
1920
                        public int getDepth() {
1921
                                return depth;
2✔
1922
                        }
1923

1924
                        @Override
1925
                        public List<ConnectionInfo> getConnections() {
1926
                                return connections;
2✔
1927
                        }
1928

1929
                        @Override
1930
                        public List<BoardCoordinates> getBoards() {
1931
                                return boards;
2✔
1932
                        }
1933

1934
                        @Override
1935
                        public PowerState getPower() {
1936
                                try (var conn = getConnection();
2✔
1937
                                                var power = conn.query(GET_SUM_BOARDS_POWERED)) {
2✔
1938
                                        return conn.transaction(false,
2✔
1939
                                                        () -> power.call1(integer("total_on"), id)
2✔
1940
                                                                        .map(totalOn -> totalOn < boardIds.size()
2!
1941
                                                                                        ? OFF
2✔
1942
                                                                                        : ON)
×
1943
                                                                        .orElse(null));
2✔
1944
                                }
1945
                        }
1946

1947
                        @Override
1948
                        public void setPower(PowerState ps) {
1949
                                if (partial) {
2!
1950
                                        throw new PartialJobException();
×
1951
                                }
1952
                                powerController.setPower(id, ps, READY);
2✔
1953
                        }
2✔
1954
                }
1955
        }
1956

1957
        /**
1958
         * Board location implementation. Does not retain database connections after
1959
         * creation.
1960
         *
1961
         * @author Donal Fellows
1962
         */
1963
        private final class BoardLocationImpl implements BoardLocation {
1964
                private JobImpl job;
1965

1966
                private final String machineName;
1967

1968
                private final int machineWidth;
1969

1970
                private final int machineHeight;
1971

1972
                private final ChipLocation chip;
1973

1974
                private final ChipLocation boardChip;
1975

1976
                private final BoardCoordinates logical;
1977

1978
                private final BoardPhysicalCoordinates physical;
1979

1980
                // Transaction is open
1981
                private BoardLocationImpl(Row row, Machine machine) {
2✔
1982
                        machineName = row.getString("machine_name");
2✔
1983
                        logical = new BoardCoordinates(row.getInt("x"), row.getInt("y"),
2✔
1984
                                        row.getInt("z"));
2✔
1985
                        physical = new BoardPhysicalCoordinates(row.getInt("cabinet"),
2✔
1986
                                        row.getInt("frame"), row.getInteger("board_num"));
2✔
1987
                        chip = row.getChip("chip_x", "chip_y");
2✔
1988
                        machineWidth = machine.getWidth();
2✔
1989
                        machineHeight = machine.getHeight();
2✔
1990
                        var boardX = row.getInteger("board_chip_x");
2✔
1991
                        if (nonNull(boardX)) {
2!
1992
                                boardChip = row.getChip("board_chip_x", "board_chip_y");
2✔
1993
                        } else {
1994
                                boardChip = chip;
×
1995
                        }
1996

1997
                        var jobId = row.getInteger("job_id");
2✔
1998
                        if (nonNull(jobId)) {
2✔
1999
                                job = new JobImpl(jobId, machine.getId());
2✔
2000
                                job.chipRoot = row.getChip("job_root_chip_x",
2✔
2001
                                                "job_root_chip_y");
2002
                        }
2003
                }
2✔
2004

2005
                @Override
2006
                public ChipLocation getBoardChip() {
2007
                        return boardChip;
2✔
2008
                }
2009

2010
                @Override
2011
                public ChipLocation getChipRelativeTo(ChipLocation rootChip) {
2012
                        int x = chip.getX() - rootChip.getX();
2✔
2013
                        if (x < 0) {
2!
2014
                                x += machineWidth * TRIAD_CHIP_SIZE;
×
2015
                        }
2016
                        int y = chip.getY() - rootChip.getY();
2✔
2017
                        if (y < 0) {
2!
2018
                                y += machineHeight * TRIAD_CHIP_SIZE;
×
2019
                        }
2020
                        return new ChipLocation(x, y);
2✔
2021
                }
2022

2023
                @Override
2024
                public String getMachine() {
2025
                        return machineName;
2✔
2026
                }
2027

2028
                @Override
2029
                public BoardCoordinates getLogical() {
2030
                        return logical;
2✔
2031
                }
2032

2033
                @Override
2034
                public BoardPhysicalCoordinates getPhysical() {
2035
                        return physical;
2✔
2036
                }
2037

2038
                @Override
2039
                public ChipLocation getChip() {
2040
                        return chip;
2✔
2041
                }
2042

2043
                @Override
2044
                public Job getJob() {
2045
                        return job;
2✔
2046
                }
2047
        }
2048

2049
        static class PartialJobException extends IllegalStateException {
2050
                private static final long serialVersionUID = 2997856394666135483L;
2051

2052
                PartialJobException() {
2053
                        super("partial job only");
×
2054
                }
×
2055
        }
2056
}
2057

2058
class ReportRollbackExn extends RuntimeException {
2059
        private static final long serialVersionUID = 1L;
2060

2061
        @FormatMethod
2062
        ReportRollbackExn(String msg, Object... args) {
2063
                super(format(msg, args));
×
2064
        }
×
2065

2066
        ReportRollbackExn(HasChipLocation chip) {
2067
                this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
2068
                                chip.getY());
×
2069
        }
×
2070
}
2071

2072
abstract class GroupsException extends RuntimeException {
2073
        private static final long serialVersionUID = 6607077117924279611L;
2074

2075
        GroupsException(String message) {
2076
                super(message);
×
2077
        }
×
2078

2079
        GroupsException(String message, Throwable cause) {
2080
                super(message, cause);
×
2081
        }
×
2082
}
2083

2084
class NoSuchGroupException extends GroupsException {
2085
        private static final long serialVersionUID = 5193818294198205503L;
2086

2087
        @FormatMethod
2088
        NoSuchGroupException(String msg, Object... args) {
2089
                super(format(msg, args));
×
2090
        }
×
2091
}
2092

2093
class MultipleGroupsException extends GroupsException {
2094
        private static final long serialVersionUID = 6284332340565334236L;
2095

2096
        @FormatMethod
2097
        MultipleGroupsException(String msg, Object... args) {
2098
                super(format(msg, args));
×
2099
        }
×
2100
}
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