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

SpiNNakerManchester / JavaSpiNNaker / 6310285782

26 Sep 2023 08:47AM UTC coverage: 36.367% (-0.5%) from 36.866%
6310285782

Pull #658

github

dkfellows
Merge branch 'master' into java-17
Pull Request #658: Update Java version to 17 and JEE to 9

1675 of 1675 new or added lines in 266 files covered. (100.0%)

8368 of 23010 relevant lines covered (36.37%)

0.36 hits per line

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

74.74
/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.utils.CollectionUtils.copy;
36
import static uk.ac.manchester.spinnaker.utils.OptionalUtils.apply;
37

38
import java.io.Serial;
39
import java.time.Duration;
40
import java.time.Instant;
41
import java.util.ArrayList;
42
import java.util.Collection;
43
import java.util.Formatter;
44
import java.util.HashMap;
45
import java.util.List;
46
import java.util.Locale;
47
import java.util.Map;
48
import java.util.Optional;
49
import java.util.Set;
50

51
import org.slf4j.Logger;
52
import org.springframework.beans.factory.annotation.Autowired;
53
import org.springframework.stereotype.Service;
54

55
import com.fasterxml.jackson.annotation.JsonIgnore;
56
import com.google.errorprone.annotations.FormatMethod;
57
import com.google.errorprone.annotations.concurrent.GuardedBy;
58

59
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.AllocatorProperties;
60
import uk.ac.manchester.spinnaker.alloc.admin.ReportMailSender;
61
import uk.ac.manchester.spinnaker.alloc.allocator.Epochs.Epoch;
62
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
63
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Query;
64
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Update;
65
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
66
import uk.ac.manchester.spinnaker.alloc.db.Row;
67
import uk.ac.manchester.spinnaker.alloc.model.BoardCoords;
68
import uk.ac.manchester.spinnaker.alloc.model.ConnectionInfo;
69
import uk.ac.manchester.spinnaker.alloc.model.Direction;
70
import uk.ac.manchester.spinnaker.alloc.model.DownLink;
71
import uk.ac.manchester.spinnaker.alloc.model.JobDescription;
72
import uk.ac.manchester.spinnaker.alloc.model.JobListEntryRecord;
73
import uk.ac.manchester.spinnaker.alloc.model.JobState;
74
import uk.ac.manchester.spinnaker.alloc.model.MachineDescription;
75
import uk.ac.manchester.spinnaker.alloc.model.MachineDescription.JobInfo;
76
import uk.ac.manchester.spinnaker.alloc.model.MachineListEntryRecord;
77
import uk.ac.manchester.spinnaker.alloc.model.PowerState;
78
import uk.ac.manchester.spinnaker.alloc.proxy.ProxyCore;
79
import uk.ac.manchester.spinnaker.alloc.security.Permit;
80
import uk.ac.manchester.spinnaker.alloc.web.IssueReportRequest;
81
import uk.ac.manchester.spinnaker.alloc.web.IssueReportRequest.ReportedBoard;
82
import uk.ac.manchester.spinnaker.machine.ChipLocation;
83
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
84
import uk.ac.manchester.spinnaker.machine.HasCoreLocation;
85
import uk.ac.manchester.spinnaker.machine.board.BMPCoords;
86
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
87
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
88
import uk.ac.manchester.spinnaker.spalloc.messages.BoardCoordinates;
89
import uk.ac.manchester.spinnaker.spalloc.messages.BoardPhysicalCoordinates;
90

91
/**
92
 * The core implementation of the Spalloc service.
93
 *
94
 * @author Donal Fellows
95
 */
96
@Service
97
public class Spalloc extends DatabaseAwareBean implements SpallocAPI {
1✔
98
        private static final String NO_BOARD_MSG =
99
                        "request does not identify an existing board "
100
                                        + "or uses a prohibited coordinate";
101

102
        private static final Logger log = getLogger(Spalloc.class);
1✔
103

104
        @Autowired
105
        private PowerController powerController;
106

107
        @Autowired
108
        private Epochs epochs;
109

110
        @Autowired
111
        private QuotaManager quotaManager;
112

113
        @Autowired
114
        private ReportMailSender emailSender;
115

116
        @Autowired
117
        private AllocatorProperties props;
118

119
        @Autowired
120
        private ProxyRememberer rememberer;
121

122
        @Autowired
123
        private AllocatorTask allocator;
124

125
        @GuardedBy("this")
1✔
126
        private transient Map<String, List<BoardCoords>> downBoardsCache =
127
                        new HashMap<>();
128

129
        @GuardedBy("this")
1✔
130
        private transient Map<String, List<DownLink>> downLinksCache =
131
                        new HashMap<>();
132

133
        @Override
134
        public Map<String, Machine> getMachines(boolean allowOutOfService) {
135
                return executeRead(c -> getMachines(c, allowOutOfService));
1✔
136
        }
137

138
        private Map<String, Machine> getMachines(Connection conn,
139
                        boolean allowOutOfService) {
140
                try (var listMachines = conn.query(GET_ALL_MACHINES)) {
1✔
141
                        return Row.stream(listMachines.call(
1✔
142
                                        row -> new MachineImpl(conn, row), allowOutOfService))
1✔
143
                                        .toMap(Machine::getName, (m) -> m);
1✔
144
                }
145
        }
146

147
        private final class ListMachinesSQL extends AbstractSQL {
1✔
148
                private final Query listMachines = conn.query(GET_ALL_MACHINES);
1✔
149

150
                private final Query countMachineThings =
1✔
151
                                conn.query(COUNT_MACHINE_THINGS);
1✔
152

153
                private final Query getTags = conn.query(GET_TAGS);
1✔
154

155
                @Override
156
                public void close() {
157
                        listMachines.close();
1✔
158
                        countMachineThings.close();
1✔
159
                        getTags.close();
1✔
160
                        super.close();
1✔
161
                }
1✔
162

163
                private MachineListEntryRecord makeMachineListEntryRecord(Row row) {
164
                        int id = row.getInt("machine_id");
1✔
165
                        var rec = new MachineListEntryRecord();
1✔
166
                        rec.setId(id);
1✔
167
                        rec.setName(row.getString("machine_name"));
1✔
168
                        if (!countMachineThings.call1((m) -> {
1✔
169
                                rec.setNumBoards(m.getInt("board_count"));
1✔
170
                                rec.setNumInUse(m.getInt("in_use"));
1✔
171
                                rec.setNumJobs(m.getInt("num_jobs"));
1✔
172
                                rec.setTags(getTags.call(string("tag"), id));
1✔
173
                                // We have to return something but don't read the result
174
                                return true;
1✔
175
                        }, id).isPresent()) {
1✔
176
                                throw new AssertionError("No result from count machine things");
×
177
                        }
178
                        return rec;
1✔
179
                }
180
        }
181

182
        @Override
183
        public List<MachineListEntryRecord>
184
                        listMachines(boolean allowOutOfService) {
185
                try (var sql = new ListMachinesSQL()) {
1✔
186
                        return sql.transactionRead(
1✔
187
                                        () -> sql.listMachines.call(
1✔
188
                                                        sql::makeMachineListEntryRecord,
1✔
189
                                                        allowOutOfService));
1✔
190
                }
191
        }
192

193
        @Override
194
        public Optional<Machine> getMachine(String name,
195
                        boolean allowOutOfService) {
196
                return executeRead(
1✔
197
                                conn -> getMachine(name, allowOutOfService, conn).map(m -> m));
1✔
198
        }
199

200
        private Optional<MachineImpl> getMachine(int id, boolean allowOutOfService,
201
                        Connection conn) {
202
                try (var idMachine = conn.query(GET_MACHINE_BY_ID)) {
1✔
203
                        return idMachine.call1(row -> new MachineImpl(conn, row),
1✔
204
                                        id, allowOutOfService);
1✔
205
                }
206
        }
207

208
        private Optional<MachineImpl> getMachine(String name,
209
                        boolean allowOutOfService, Connection conn) {
210
                try (var namedMachine = conn.query(GET_NAMED_MACHINE)) {
1✔
211
                        return namedMachine.call1(row -> new MachineImpl(conn, row),
1✔
212
                                        name, allowOutOfService);
1✔
213
                }
214
        }
215

216
        private final class DescribeMachineSQL extends AbstractSQL {
1✔
217
                final Query namedMachine = conn.query(GET_NAMED_MACHINE);
1✔
218

219
                final Query countMachineThings = conn.query(COUNT_MACHINE_THINGS);
1✔
220

221
                final Query getTags = conn.query(GET_TAGS);
1✔
222

223
                final Query getJobs = conn.query(GET_MACHINE_JOBS);
1✔
224

225
                final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
1✔
226

227
                final Query getLive = conn.query(GET_LIVE_BOARDS);
1✔
228

229
                final Query getDead = conn.query(GET_DEAD_BOARDS);
1✔
230

231
                final Query getQuota = conn.query(GET_USER_QUOTA);
1✔
232

233
                @Override
234
                public void close() {
235
                        namedMachine.close();
1✔
236
                        countMachineThings.close();
1✔
237
                        getTags.close();
1✔
238
                        getJobs.close();
1✔
239
                        getCoords.close();
1✔
240
                        getLive.close();
1✔
241
                        getDead.close();
1✔
242
                        getQuota.close();
1✔
243
                        super.close();
1✔
244
                }
1✔
245
        }
246

247
        @Override
248
        public Optional<MachineDescription> getMachineInfo(String machine,
249
                        boolean allowOutOfService, Permit permit) {
250
                try (var sql = new DescribeMachineSQL()) {
1✔
251
                        return sql.transactionRead(() -> apply(
1✔
252
                                        sql.namedMachine.call1(Spalloc::getBasicMachineInfo,
1✔
253
                                                        machine, allowOutOfService),
1✔
254
                                        md -> sql.countMachineThings.call1(integer("in_use"),
1✔
255
                                                        md.getId()).ifPresent(md::setNumInUse),
1✔
256
                                        md -> md.setTags(
1✔
257
                                                        sql.getTags.call(string("tag"), md.getId())),
1✔
258
                                        md -> md.setJobs(sql.getJobs.call(
1✔
259
                                                        row -> getMachineJobInfo(permit, sql.getCoords,
1✔
260
                                                                        row),
261
                                                        md.getId())),
1✔
262
                                        md -> md.setLive(sql.getLive.call(
1✔
263
                                                        row -> new BoardCoords(row, !permit.admin),
×
264
                                                        md.getId())),
1✔
265
                                        md -> md.setDead(sql.getDead.call(
1✔
266
                                                        row -> new BoardCoords(row, !permit.admin),
×
267
                                                        md.getId())),
1✔
268
                                        md -> sql.getQuota.call1(int64("quota_total"), permit.name)
1✔
269
                                                        .ifPresent(md::setQuota)));
1✔
270
                }
271
        }
272

273
        private static MachineDescription getBasicMachineInfo(Row row) {
274
                var md = new MachineDescription();
1✔
275
                md.setId(row.getInt("machine_id"));
1✔
276
                md.setName(row.getString("machine_name"));
1✔
277
                md.setWidth(row.getInt("width"));
1✔
278
                md.setHeight(row.getInt("height"));
1✔
279
                return md;
1✔
280
        }
281

282
        private static JobInfo getMachineJobInfo(Permit permit, Query getCoords,
283
                        Row row) {
284
                int jobId = row.getInt("job_id");
1✔
285
                var mayUnveil = permit.unveilFor(row.getString("owner_name"));
1✔
286
                var owner = mayUnveil ? row.getString("owner_name") : null;
1✔
287

288
                var ji = new JobInfo();
1✔
289
                ji.setId(jobId);
1✔
290
                ji.setOwner(owner);
1✔
291
                ji.setBoards(
1✔
292
                                getCoords.call(r -> new BoardCoords(r, !mayUnveil), jobId));
1✔
293
                return ji;
1✔
294
        }
295

296
        @Override
297
        public Jobs getJobs(boolean deleted, int limit, int start) {
298
                return executeRead(conn -> {
1✔
299
                        if (deleted) {
1✔
300
                                try (var jobs = conn.query(GET_JOB_IDS)) {
1✔
301
                                        return new JobCollection(
1✔
302
                                                        jobs.call(this::makeJob, limit, start));
1✔
303
                                }
304
                        } else {
305
                                try (var jobs = conn.query(GET_LIVE_JOB_IDS)) {
1✔
306
                                        return new JobCollection(
1✔
307
                                                        jobs.call(this::makeJob, limit, start));
1✔
308
                                }
309
                        }
310
                });
311
        }
312

313
        /**
314
         * Makes "partial" jobs; some fields are shrouded, modifications are
315
         * disabled.
316
         *
317
         * @param row
318
         *            The row to make the job from.
319
         */
320
        private Job makeJob(Row row) {
321
                int jobId = row.getInt("job_id");
1✔
322
                int machineId = row.getInt("machine_id");
1✔
323
                var jobState = row.getEnum("job_state", JobState.class);
1✔
324
                var keepalive = row.getInstant("keepalive_timestamp");
1✔
325
                return new JobImpl(jobId, machineId, jobState, keepalive);
1✔
326
        }
327

328
        @Override
329
        public List<JobListEntryRecord> listJobs(Permit permit) {
330
                return executeRead(conn -> {
1✔
331
                        try (var listLiveJobs = conn.query(LIST_LIVE_JOBS);
1✔
332
                                        var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
1✔
333
                                        var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
1✔
334
                                return listLiveJobs.call(row -> makeJobListEntryRecord(permit,
1✔
335
                                                countPoweredBoards, getCoords, row));
336
                        }
337
                });
338
        }
339

340
        private static JobListEntryRecord makeJobListEntryRecord(Permit permit,
341
                        Query countPoweredBoards, Query getCoords, Row row) {
342
                var rec = new JobListEntryRecord();
1✔
343
                int id = row.getInt("job_id");
1✔
344
                rec.setId(id);
1✔
345
                rec.setState(row.getEnum("job_state", JobState.class));
1✔
346
                var numBoards = row.getInteger("allocation_size");
1✔
347
                rec.setPowered(nonNull(numBoards) && numBoards.equals(countPoweredBoards
1✔
348
                                .call1(integer("c"), id).orElseThrow()));
×
349
                rec.setMachineId(row.getInt("machine_id"));
1✔
350
                rec.setMachineName(row.getString("machine_name"));
1✔
351
                rec.setCreationTimestamp(row.getInstant("create_timestamp"));
1✔
352
                rec.setKeepaliveInterval(row.getDuration("keepalive_interval"));
1✔
353
                rec.setBoards(getCoords.call(r -> new BoardCoords(r, false), id));
1✔
354
                rec.setOriginalRequest(row.getBytes("original_request"));
1✔
355
                var owner = row.getString("user_name");
1✔
356
                if (permit.unveilFor(owner)) {
1✔
357
                        rec.setOwner(owner);
1✔
358
                        rec.setHost(row.getString("keepalive_host"));
1✔
359
                }
360
                return rec;
1✔
361
        }
362

363
        @Override
364
        public Optional<Job> getJob(Permit permit, int id) {
365
                return executeRead(conn -> getJob(id, conn).map(j -> (Job) j));
1✔
366
        }
367

368
        private Optional<JobImpl> getJob(int id, Connection conn) {
369
                try (var s = conn.query(GET_JOB)) {
1✔
370
                        return s.call1(row -> new JobImpl(conn, row), id);
1✔
371
                }
372
        }
373

374
        @Override
375
        public Optional<JobDescription> getJobInfo(Permit permit, int id) {
376
                return executeRead(conn -> {
1✔
377
                        try (var s = conn.query(GET_JOB);
1✔
378
                                        var chipDimensions = conn.query(GET_JOB_CHIP_DIMENSIONS);
1✔
379
                                        var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
1✔
380
                                        var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
1✔
381
                                return s.call1(row -> jobDescription(id, row,
1✔
382
                                                chipDimensions, countPoweredBoards, getCoords), id);
1✔
383
                        }
384
                });
385
        }
386

387
        private static JobDescription jobDescription(int id, Row job,
388
                        Query chipDimensions, Query countPoweredBoards, Query getCoords) {
389
                /*
390
                 * We won't deliver this object to the front end unless they are allowed
391
                 * to see it in its entirety.
392
                 */
393
                var jd = new JobDescription();
1✔
394
                jd.setId(id);
1✔
395
                jd.setMachine(job.getString("machine_name"));
1✔
396
                jd.setState(job.getEnum("job_state", JobState.class));
1✔
397
                jd.setOwner(job.getString("owner"));
1✔
398
                jd.setOwnerHost(job.getString("keepalive_host"));
1✔
399
                jd.setStartTime(job.getInstant("create_timestamp"));
1✔
400
                jd.setKeepAlive(job.getDuration("keepalive_interval"));
1✔
401
                jd.setRequestBytes(job.getBytes("original_request"));
1✔
402
                chipDimensions.call1(cd -> {
1✔
403
                        jd.setWidth(cd.getInt("width"));
1✔
404
                        jd.setHeight(cd.getInt("height"));
1✔
405
                        // We have to return something but we ignore it anyway
406
                        return true;
1✔
407
                }, id);
1✔
408
                int poweredCount =
1✔
409
                                countPoweredBoards.call1(integer("c"), id).orElseThrow();
1✔
410
                jd.setBoards(getCoords.call(r -> new BoardCoords(r, false), id));
1✔
411
                jd.setPowered(jd.getBoards().size() == poweredCount);
1✔
412
                return jd;
1✔
413
        }
414

415
        @Override
416
        public Optional<Job> createJobInGroup(String owner, String groupName,
417
                        CreateDescriptor descriptor, String machineName, List<String> tags,
418
                        Duration keepaliveInterval, byte[] req) {
419
                return execute(conn -> {
1✔
420
                        int user = getUser(conn, owner).orElseThrow(
1✔
421
                                        () -> new RuntimeException("no such user: " + owner));
×
422
                        int group = selectGroup(conn, owner, groupName);
1✔
423
                        if (!quotaManager.mayCreateJob(group)) {
1✔
424
                                // No quota left
425
                                return Optional.empty();
×
426
                        }
427

428
                        var m = selectMachine(conn, machineName, tags);
1✔
429
                        if (!m.isPresent()) {
1✔
430
                                // Cannot find machine!
431
                                return Optional.empty();
×
432
                        }
433
                        var machine = m.orElseThrow();
1✔
434

435
                        var id = insertJob(conn, machine, user, group, keepaliveInterval,
1✔
436
                                        req);
437
                        if (!id.isPresent()) {
1✔
438
                                // Insert failed
439
                                return Optional.empty();
×
440
                        }
441
                        int jobId = id.orElseThrow();
1✔
442

443
                        var scale = props.getPriorityScale();
1✔
444

445
                        if (machine.getArea() < descriptor.getArea()) {
1✔
446
                                throw new IllegalArgumentException(
×
447
                                                "request cannot fit on machine");
448
                        }
449

450
                        // Ask the allocator engine to do the allocation
451
                        int numBoards = descriptor.visit(new CreateVisitor<Integer>() {
1✔
452
                                @Override
453
                                public Integer numBoards(CreateNumBoards nb) {
454
                                        try (var insertReq = conn.update(INSERT_REQ_N_BOARDS)) {
1✔
455
                                                insertReq.call(jobId, nb.numBoards, nb.maxDead,
1✔
456
                                                                (int) (nb.getArea() * scale.getSize()));
1✔
457
                                        }
458
                                        return nb.numBoards;
1✔
459
                                }
460

461
                                @Override
462
                                public Integer dimensions(CreateDimensions d) {
463
                                        try (var insertReq = conn.update(INSERT_REQ_SIZE)) {
1✔
464
                                                insertReq.call(jobId, d.width, d.height, d.maxDead,
1✔
465
                                                                (int) (d.getArea() * scale.getDimensions()));
1✔
466
                                        }
467
                                        return max(1, d.getArea() - d.maxDead);
1✔
468
                                }
469

470
                                /*
471
                                 * Request by area rooted at specific location; resolve to board
472
                                 * ID now, as that doesn't depend on whether the board is
473
                                 * currently in use.
474
                                 */
475
                                @Override
476
                                public Integer dimensionsAt(CreateDimensionsAt da) {
477
                                        try (var insertReq = conn.update(INSERT_REQ_SIZE_BOARD)) {
1✔
478
                                                insertReq.call(jobId,
1✔
479
                                                                locateBoard(conn, machine.name, da, true),
1✔
480
                                                                da.width, da.height, da.maxDead,
1✔
481
                                                                (int) scale.getSpecificBoard());
1✔
482
                                        }
483
                                        return max(1, da.getArea() - da.maxDead);
1✔
484
                                }
485

486
                                /*
487
                                 * Request by specific location; resolve to board ID now, as
488
                                 * that doesn't depend on whether the board is currently in
489
                                 * use.
490
                                 */
491
                                @Override
492
                                public Integer board(CreateBoard b) {
493
                                        try (var insertReq = conn.update(INSERT_REQ_BOARD)) {
1✔
494
                                                /*
495
                                                 * This doesn't pass along the max dead boards; only
496
                                                 * after one!
497
                                                 */
498
                                                insertReq.call(jobId,
1✔
499
                                                                locateBoard(conn, machine.name, b, false),
1✔
500
                                                                (int) scale.getSpecificBoard());
1✔
501
                                        }
502
                                        return 1;
1✔
503
                                }
504
                        });
505

506
                        // DB now changed; can report success
507
                        JobLifecycle.log.info(
1✔
508
                                        "created job {} on {} for {} asking for {} board(s)", jobId,
1✔
509
                                        machine.name, owner, numBoards);
1✔
510

511
                        allocator.scheduleAllocateNow();
1✔
512
                        return getJob(jobId, conn).map(ji -> (Job) ji);
1✔
513
                });
514
        }
515

516
        @Override
517
        public Optional<Job> createJob(String owner, CreateDescriptor descriptor,
518
                        String machineName, List<String> tags, Duration keepaliveInterval,
519
                        byte[] originalRequest) {
520
                return execute(conn -> createJobInGroup(
1✔
521
                                owner, getOnlyGroup(conn, owner), descriptor, machineName,
1✔
522
                                tags, keepaliveInterval, originalRequest));
523
        }
524

525
        @Override
526
        public Optional<Job> createJobInCollabSession(String owner,
527
                        String nmpiCollab, CreateDescriptor descriptor,
528
                        String machineName, List<String> tags, Duration keepaliveInterval,
529
                        byte[] originalRequest) {
530
                var quotaUnits = quotaManager.mayCreateNMPISession(nmpiCollab);
×
531
                if (quotaUnits.isEmpty()) {
×
532
                        return Optional.empty();
×
533
                }
534

535
                // Use the Collab name as the group, as it should exist
536
                var job = execute(conn -> createJobInGroup(
×
537
                                owner, nmpiCollab, descriptor, machineName,
538
                                tags, keepaliveInterval, originalRequest));
539
                // On failure to get job, just return; shouldn't happen as quota checked
540
                // earlier, but just in case!
541
                if (job.isEmpty()) {
×
542
                        return job;
×
543
                }
544

545
                quotaManager.associateNMPISession(
×
546
                                job.get().getId(), owner, nmpiCollab, quotaUnits.get());
×
547

548
                // Return the job created
549
                return job;
×
550
        }
551

552
        @Override
553
        public Optional<Job> createJobForNMPIJob(String owner, int nmpiJobId,
554
                        CreateDescriptor descriptor, String machineName, List<String> tags,
555
                        Duration keepaliveInterval,        byte[] originalRequest) {
556
                var collab = quotaManager.mayUseNMPIJob(owner, nmpiJobId);
×
557
                if (collab.isEmpty()) {
×
558
                        return Optional.empty();
×
559
                }
560
                var quotaDetails = collab.get();
×
561

562
                var job = execute(conn -> createJobInGroup(
×
563
                                owner, quotaDetails.collabId(), descriptor, machineName,
×
564
                                tags, keepaliveInterval, originalRequest));
565
                // On failure to get job, just return; shouldn't happen as quota checked
566
                // earlier, but just in case!
567
                if (job.isEmpty()) {
×
568
                        return job;
×
569
                }
570

571
                quotaManager.associateNMPIJob(job.get().getId(), nmpiJobId,
×
572
                                quotaDetails.quotaUnits());
×
573

574
                // Return the job created
575
                return job;
×
576
        }
577

578
        /**
579
         * Get the specified group.
580
         *
581
         * @param conn
582
         *            DB connection
583
         * @param user
584
         *            Who is the user?
585
         * @param groupName
586
         *            What group did they specify? (May be {@code null} to say "pick
587
         *            the unique valid possibility for the owner".)
588
         * @return The group ID.
589
         * @throws GroupsException
590
         *             If we can't get a definite group to account against.
591
         */
592
        private int selectGroup(Connection conn, String user, String groupName) {
593
                try (var getGroup = conn.query(GET_GROUP_BY_NAME_AND_MEMBER)) {
1✔
594
                        return getGroup.call1(integer("group_id"), user, groupName)
1✔
595
                                        .orElseThrow(() -> new NoSuchGroupException(
1✔
596
                                                        "group %s does not exist or %s "
597
                                                                        + "is not a member of it",
598
                                                        groupName, user));
599
                }
600
        }
601

602
        private String getOnlyGroup(Connection conn, String user) {
603
                try (var listGroups = conn.query(GET_GROUP_NAMES_OF_USER)) {
1✔
604
                        // No name given; need to guess.
605
                        var groups = listGroups.call(row -> row.getString("group_name"),
1✔
606
                                        user);
607
                        if (groups.size() > 1) {
1✔
608
                                throw new NoSuchGroupException(
×
609
                                                "User is a member of more than one group, so the group"
610
                                                + " must be selected in the request");
611
                        }
612
                        if (groups.size() == 0) {
1✔
613
                                throw new NoSuchGroupException(
×
614
                                                "User is not a member of any group!");
615
                        }
616
                        return groups.get(0);
1✔
617
                }
618
        }
619

620
        private static Optional<Integer> getUser(Connection conn, String userName) {
621
                try (var getUser = conn.query(GET_USER_ID)) {
1✔
622
                        return getUser.call1(integer("user_id"), userName);
1✔
623
                }
624
        }
625

626
        /**
627
         * Resolve a machine name and {@link HasBoardCoords} to a board identifier.
628
         *
629
         * @param conn
630
         *            How to get to the DB.
631
         * @param machineName
632
         *            The name of the machine.
633
         * @param b
634
         *            The request that is the coordinate holder.
635
         * @param requireTriadRoot
636
         *            Whether we require the Z coordinate to be zero.
637
         * @return The board ID.
638
         * @throws IllegalArgumentException
639
         *             If the board doesn't exist or it is a board that is not a
640
         *             root of a triad when a triad root is required.
641
         */
642
        private Integer locateBoard(Connection conn, String machineName,
643
                        HasBoardCoords b, boolean requireTriadRoot) {
644
                record BoardLocated(int boardId, int z) {
1✔
645
                        BoardLocated(Row row) {
646
                                this(row.getInt("board_id"), row.getInt("z"));
1✔
647
                        }
1✔
648
                }
649

650
                try (var findTriad = conn.query(FIND_BOARD_BY_NAME_AND_XYZ);
1✔
651
                                var findPhysical = conn.query(FIND_BOARD_BY_NAME_AND_CFB);
1✔
652
                                var findIP = conn.query(FIND_BOARD_BY_NAME_AND_IP_ADDRESS)) {
1✔
653
                        if (nonNull(b.triad)) {
1✔
654
                                return findTriad.call1(BoardLocated::new,
1✔
655
                                                machineName, b.triad.x(), b.triad.y(), b.triad.z())
1✔
656
                                                .filter(board -> !requireTriadRoot || board.z == 0)
1✔
657
                                                .map(board -> board.boardId)
1✔
658
                                                .orElseThrow(() -> new IllegalArgumentException(
1✔
659
                                                                NO_BOARD_MSG));
660
                        } else if (nonNull(b.physical)) {
1✔
661
                                return findPhysical.call1(
×
662
                                                BoardLocated::new, machineName, b.physical.c(),
×
663
                                                                b.physical.f(), b.physical.b())
×
664
                                                .filter(board -> !requireTriadRoot || board.z == 0)
×
665
                                                .map(board -> board.boardId)
×
666
                                                .orElseThrow(() -> new IllegalArgumentException(
×
667
                                                                NO_BOARD_MSG));
668
                        } else {
669
                                return findIP.call1(BoardLocated::new, machineName, b.ip)
1✔
670
                                                .filter(board -> !requireTriadRoot || board.z == 0)
1✔
671
                                                .map(board -> board.boardId)
1✔
672
                                                .orElseThrow(() -> new IllegalArgumentException(
1✔
673
                                                                NO_BOARD_MSG));
674
                        }
675
                }
1✔
676
        }
677

678
        private static Optional<Integer> insertJob(Connection conn, MachineImpl m,
679
                        int owner, int group, Duration keepaliveInterval, byte[] req) {
680
                try (var makeJob = conn.update(INSERT_JOB)) {
1✔
681
                        return makeJob.key(m.id, owner, group, keepaliveInterval, req);
1✔
682
                }
683
        }
684

685
        private Optional<MachineImpl> selectMachine(Connection conn,
686
                        String machineName, List<String> tags) {
687
                if (nonNull(machineName)) {
1✔
688
                        return getMachine(machineName, false, conn);
1✔
689
                } else if (!tags.isEmpty()) {
×
690
                        for (var m : getMachines(conn, false).values()) {
×
691
                                var mi = (MachineImpl) m;
×
692
                                if (mi.tags.containsAll(tags)) {
×
693
                                        /*
694
                                         * Originally, spalloc checked if allocation was possible;
695
                                         * we just assume that it is because there really isn't ever
696
                                         * going to be that many different machines on one service.
697
                                         */
698
                                        return Optional.of(mi);
×
699
                                }
700
                        }
×
701
                }
702
                return Optional.empty();
×
703
        }
704

705
        @Override
706
        public void purgeDownCache() {
707
                synchronized (this) {
×
708
                        downBoardsCache.clear();
×
709
                        downLinksCache.clear();
×
710
                }
×
711
        }
×
712

713
        private static String mergeDescription(HasChipLocation coreLocation,
714
                        String description) {
715
                if (isNull(description)) {
1✔
716
                        description = "<null>";
×
717
                }
718
                if (coreLocation instanceof HasCoreLocation loc) {
1✔
719
                        description += format(" (at core %d of chip %s)", loc.getP(),
×
720
                                        loc.asChipLocation());
×
721
                } else if (nonNull(coreLocation)) {
1✔
722
                        description +=
×
723
                                        format(" (at chip %s)", coreLocation.asChipLocation());
×
724
                }
725
                return description;
1✔
726
        }
727

728
        private record Problem(int boardId, Integer jobId) {
1✔
729
                Problem(Row row) {
730
                        this(row.getInt("board_id"), row.getInt("job_id"));
1✔
731
                }
1✔
732
        }
733

734
        @Override
735
        public void reportProblem(String address, HasChipLocation coreLocation,
736
                        String description, Permit permit) {
737
                try (var sql = new BoardReportSQL()) {
1✔
738
                        var desc = mergeDescription(coreLocation, description);
1✔
739
                        var email = sql.transaction(() -> {
1✔
740
                                var machines = getMachines(sql.getConnection(), true).values();
1✔
741
                                for (var m : machines) {
1✔
742
                                        var mail = sql.findBoardNet.call1(
1✔
743
                                                        Problem::new, m.getId(), address)
1✔
744
                                                        .flatMap(prob -> reportProblem(prob, desc, permit,
1✔
745
                                                                        sql));
746
                                        if (mail.isPresent()) {
1✔
747
                                                return mail;
×
748
                                        }
749
                                }
1✔
750
                                return Optional.empty();
1✔
751
                        });
752
                        // Outside the transaction!
753
                        email.ifPresent(emailSender::sendServiceMail);
1✔
754
                } catch (ReportRollbackExn e) {
×
755
                        log.warn("failed to handle problem report", e);
×
756
                }
1✔
757
        }
1✔
758

759
        private Optional<EmailBuilder> reportProblem(Problem problem,
760
                        String description,        Permit permit, BoardReportSQL sql) {
761
                var email = new EmailBuilder(problem.jobId);
1✔
762
                email.header(description, 1, permit.name);
1✔
763
                int userId = getUser(sql.getConnection(), permit.name).orElseThrow(
1✔
764
                                () -> new ReportRollbackExn("no such user: %s", permit.name));
×
765
                sql.insertReport.key(problem.boardId, problem.jobId,
1✔
766
                                description, userId).ifPresent(email::issue);
1✔
767
                return takeBoardsOutOfService(sql, email).map(acted -> {
1✔
768
                        email.footer(acted);
×
769
                        return email;
×
770
                });
771
        }
772

773
        private record Reported(int boardId, int x, int y, int z, String address,
×
774
                        int numReports) {
775
                Reported(Row row) {
776
                        this(row.getInt("board_id"), row.getInt("x"), row.getInt("y"),
×
777
                                        row.getInt("z"), row.getString("address"),
×
778
                                        row.getInt("numReports"));
×
779
                }
×
780
        }
781

782
        /**
783
         * Take boards out of service if they've been reported frequently enough.
784
         *
785
         * @param sql
786
         *            How to touch the DB
787
         * @param email
788
         *            The email we are building.
789
         * @return The number of boards taken out of service
790
         */
791
        private Optional<Integer> takeBoardsOutOfService(BoardReportSQL sql,
792
                        EmailBuilder email) {
793
                int acted = 0;
1✔
794
                for (var report : sql.getReported.call(Reported::new,
1✔
795
                                props.getReportActionThreshold())) {
1✔
796
                        if (sql.setFunctioning.call(false, report.boardId) > 0) {
×
797
                                email.serviceActionDone(report);
×
798
                                acted++;
×
799
                        }
800
                }
×
801
                if (acted > 0) {
1✔
802
                        purgeDownCache();
×
803
                }
804
                return acted > 0 ? Optional.of(acted) : Optional.empty();
1✔
805
        }
806

807
        private static DownLink makeDownLinkFromRow(Row row) {
808
                // Non-standard column names to reduce number of queries
809
                var board1 = new BoardCoords(row.getInt("board_1_x"),
×
810
                                row.getInt("board_1_y"), row.getInt("board_1_z"),
×
811
                                row.getInt("board_1_c"), row.getInt("board_1_f"),
×
812
                                row.getInteger("board_1_b"), row.getString("board_1_addr"));
×
813
                var board2 = new BoardCoords(row.getInt("board_2_x"),
×
814
                                row.getInt("board_2_y"), row.getInt("board_2_z"),
×
815
                                row.getInt("board_2_c"), row.getInt("board_2_f"),
×
816
                                row.getInteger("board_2_b"), row.getString("board_2_addr"));
×
817
                return new DownLink(board1, row.getEnum("dir_1", Direction.class),
×
818
                                board2, row.getEnum("dir_2", Direction.class));
×
819
        }
820

821
        private final class MachineImpl implements Machine {
822
                private final int id;
823

824
                private final boolean inService;
825

826
                private final String name;
827

828
                private final Set<String> tags;
829

830
                private final int width;
831

832
                private final int height;
833

834
                private boolean lookedUpWraps;
835

836
                private boolean hWrap;
837

838
                private boolean vWrap;
839

840
                @JsonIgnore
841
                private final Epoch epoch;
842

843
                MachineImpl(Connection conn, Row rs) {
1✔
844
                        id = rs.getInt("machine_id");
1✔
845
                        name = rs.getString("machine_name");
1✔
846
                        width = rs.getInt("width");
1✔
847
                        height = rs.getInt("height");
1✔
848
                        inService = rs.getBoolean("in_service");
1✔
849
                        lookedUpWraps = false;
1✔
850
                        try (var getTags = conn.query(GET_TAGS)) {
1✔
851
                                tags = Row.stream(copy(getTags.call(string("tag"), id)))
1✔
852
                                                .toSet();
1✔
853
                        }
854

855
                        this.epoch = epochs.getMachineEpoch(id);
1✔
856
                }
1✔
857

858
                private int getArea() {
859
                        return width * height * TRIAD_DEPTH;
1✔
860
                }
861

862
                @Override
863
                public boolean waitForChange(Duration timeout) {
864
                        if (isNull(epoch)) {
1✔
865
                                log.info("Machine {} epoch is null!", id);
×
866
                                return true;
×
867
                        }
868
                        try {
869
                                log.info("Waiting for change in epoch for {}", id);
1✔
870
                                return epoch.waitForChange(timeout);
1✔
871
                        } catch (InterruptedException interrupted) {
1✔
872
                                log.info("Interrupted waiting for change on {}", id);
1✔
873
                                return false;
1✔
874
                        }
875
                }
876

877
                private BoardLocation boardLoc(Row row) {
878
                        return new BoardLocationImpl(row, this);
1✔
879
                }
880

881
                @Override
882
                public Optional<BoardLocation> getBoardByChip(HasChipLocation chip) {
883
                        try (var conn = getConnection();
1✔
884
                                        var findBoard = conn.query(findBoardByGlobalChip)) {
1✔
885
                                return conn.transaction(false, () -> findBoard
1✔
886
                                                .call1(this::boardLoc, id, chip.getX(), chip.getY()));
1✔
887
                        }
888
                }
889

890
                @Override
891
                public Optional<BoardLocation> getBoardByPhysicalCoords(
892
                                PhysicalCoords coords) {
893
                        try (var conn = getConnection();
1✔
894
                                        var findBoard = conn.query(findBoardByPhysicalCoords)) {
1✔
895
                                return conn.transaction(false,
1✔
896
                                                () -> findBoard.call1(this::boardLoc, id, coords.c(),
1✔
897
                                                                coords.f(), coords.b()));
1✔
898
                        }
899
                }
900

901
                @Override
902
                public Optional<BoardLocation> getBoardByLogicalCoords(
903
                                TriadCoords coords) {
904
                        try (var conn = getConnection();
1✔
905
                                        var findBoard = conn.query(findBoardByLogicalCoords)) {
1✔
906
                                return conn.transaction(false,
1✔
907
                                                () -> findBoard.call1(this::boardLoc, id, coords.x(),
1✔
908
                                                                coords.y(), coords.z()));
1✔
909
                        }
910
                }
911

912
                @Override
913
                public Optional<BoardLocation> getBoardByIPAddress(String address) {
914
                        try (var conn = getConnection();
1✔
915
                                        var findBoard = conn.query(findBoardByIPAddress)) {
1✔
916
                                return conn.transaction(false,
1✔
917
                                                () -> findBoard.call1(this::boardLoc, id, address));
1✔
918
                        }
919
                }
920

921
                @Override
922
                public String getRootBoardBMPAddress() {
923
                        try (var conn = getConnection();
1✔
924
                                        var rootBMPaddr = conn.query(GET_ROOT_BMP_ADDRESS)) {
1✔
925
                                return conn
1✔
926
                                                .transaction(false,
1✔
927
                                                                () -> rootBMPaddr.call1(string("address"), id))
1✔
928
                                                .orElse(null);
1✔
929
                        }
930
                }
931

932
                @Override
933
                public List<Integer> getBoardNumbers() {
934
                        try (var conn = getConnection();
1✔
935
                                        var boardNumbers = conn.query(GET_BOARD_NUMBERS)) {
1✔
936
                                return conn.transaction(false, () -> boardNumbers.call(
1✔
937
                                                integer("board_num"), id));
1✔
938
                        }
939
                }
940

941
                @Override
942
                public List<BoardCoords> getDeadBoards() {
943
                        // Assume that the list doesn't change for the duration of this obj
944
                        synchronized (Spalloc.this) {
1✔
945
                                var down = downBoardsCache.get(name);
1✔
946
                                if (nonNull(down)) {
1✔
947
                                        return copy(down);
1✔
948
                                }
949
                        }
1✔
950
                        try (var conn = getConnection();
1✔
951
                                        var boardNumbers = conn.query(GET_DEAD_BOARDS)) {
1✔
952
                                var downBoards = conn.transaction(false,
1✔
953
                                                () -> boardNumbers.call(
1✔
954
                                                                row -> new BoardCoords(row, false), id));
1✔
955
                                synchronized (Spalloc.this) {
1✔
956
                                        downBoardsCache.putIfAbsent(name, downBoards);
1✔
957
                                }
1✔
958
                                return copy(downBoards);
1✔
959
                        }
960
                }
961

962
                @Override
963
                public List<DownLink> getDownLinks() {
964
                        // Assume that the list doesn't change for the duration of this obj
965
                        synchronized (Spalloc.this) {
1✔
966
                                var down = downLinksCache.get(name);
1✔
967
                                if (nonNull(down)) {
1✔
968
                                        return copy(down);
1✔
969
                                }
970
                        }
1✔
971
                        try (var conn = getConnection();
1✔
972
                                        var boardNumbers = conn.query(GET_DEAD_LINKS)) {
1✔
973
                                var downLinks = conn.transaction(false, () -> boardNumbers
1✔
974
                                                .call(Spalloc::makeDownLinkFromRow, id));
1✔
975
                                synchronized (Spalloc.this) {
1✔
976
                                        downLinksCache.putIfAbsent(name, downLinks);
1✔
977
                                }
1✔
978
                                return copy(downLinks);
1✔
979
                        }
980
                }
981

982
                @Override
983
                public List<Integer> getAvailableBoards() {
984
                        try (var conn = getConnection();
1✔
985
                                        var boardNumbers = conn
1✔
986
                                                        .query(GET_AVAILABLE_BOARD_NUMBERS)) {
1✔
987
                                return conn.transaction(false, () -> boardNumbers.call(
1✔
988
                                                integer("board_num"), id));
1✔
989
                        }
990
                }
991

992
                @Override
993
                public int getId() {
994
                        return id;
1✔
995
                }
996

997
                @Override
998
                public String getName() {
999
                        return name;
1✔
1000
                }
1001

1002
                @Override
1003
                public Set<String> getTags() {
1004
                        return tags;
1✔
1005
                }
1006

1007
                @Override
1008
                public int getWidth() {
1009
                        return width;
1✔
1010
                }
1011

1012
                @Override
1013
                public int getHeight() {
1014
                        return height;
1✔
1015
                }
1016

1017
                @Override
1018
                public boolean isInService() {
1019
                        return inService;
1✔
1020
                }
1021

1022
                @Override
1023
                public String getBMPAddress(BMPCoords bmp) {
1024
                        try (var conn = getConnection();
1✔
1025
                                        var bmpAddr = conn.query(GET_BMP_ADDRESS)) {
1✔
1026
                                return conn
1✔
1027
                                                .transaction(false,
1✔
1028
                                                                () -> bmpAddr.call1(string("address"), id,
1✔
1029
                                                                                bmp.cabinet(), bmp.frame()))
1✔
1030
                                                .orElse(null);
1✔
1031
                        }
1032
                }
1033

1034
                @Override
1035
                public List<Integer> getBoardNumbers(BMPCoords bmp) {
1036
                        try (var conn = getConnection();
1✔
1037
                                        var boardNumbers = conn.query(GET_BMP_BOARD_NUMBERS)) {
1✔
1038
                                return conn.transaction(false,
1✔
1039
                                                () -> boardNumbers.call(integer("board_num"), id,
1✔
1040
                                                                bmp.cabinet(), bmp.frame()));
1✔
1041
                        }
1042
                }
1043

1044
                @Override
1045
                public boolean equals(Object other) {
1046
                        // Equality is defined exactly by the database ID
1047
                        return (other instanceof MachineImpl m) && (id == m.id);
×
1048
                }
1049

1050
                @Override
1051
                public int hashCode() {
1052
                        return id;
×
1053
                }
1054

1055
                @Override
1056
                public String toString() {
1057
                        return "Machine(" + name + ")";
×
1058
                }
1059

1060
                private void retrieveWraps() {
1061
                        try (var conn = getConnection();
×
1062
                                        var getWraps = conn.query(GET_MACHINE_WRAPS)) {
×
1063
                                /*
1064
                                 * No locking; not too bothered which thread asks as result will
1065
                                 * be the same either way
1066
                                 */
1067
                                lookedUpWraps =
×
1068
                                                conn.transaction(false, () -> getWraps.call1(rs -> {
×
1069
                                                        hWrap = rs.getBoolean("horizontal_wrap");
×
1070
                                                        vWrap = rs.getBoolean("vertical_wrap");
×
1071
                                                        return true;
×
1072
                                                }, id)).orElse(false);
×
1073
                        }
1074
                }
×
1075

1076
                @Override
1077
                public boolean isHorizonallyWrapped() {
1078
                        if (!lookedUpWraps) {
×
1079
                                retrieveWraps();
×
1080
                        }
1081
                        return hWrap;
×
1082
                }
1083

1084
                @Override
1085
                public boolean isVerticallyWrapped() {
1086
                        if (!lookedUpWraps) {
×
1087
                                retrieveWraps();
×
1088
                        }
1089
                        return vWrap;
×
1090
                }
1091
        }
1092

1093
        private final class JobCollection implements Jobs {
1094
                @JsonIgnore
1095
                private final Epoch epoch;
1096

1097
                private final List<Job> jobs;
1098

1099
                private JobCollection(List<Job> jobs) {
1✔
1100
                        this.jobs = jobs;
1✔
1101
                        if (jobs.isEmpty()) {
1✔
1102
                                epoch = null;
1✔
1103
                        } else {
1104
                                epoch = epochs.getJobsEpoch(
1✔
1105
                                                jobs.stream().map(Job::getId).collect(toList()));
1✔
1106
                        }
1107
                }
1✔
1108

1109
                @Override
1110
                public boolean waitForChange(Duration timeout) {
1111
                        if (isNull(epoch)) {
×
1112
                                return true;
×
1113
                        }
1114
                        try {
1115
                                return epoch.waitForChange(timeout);
×
1116
                        } catch (InterruptedException interrupted) {
×
1117
                                currentThread().interrupt();
×
1118
                                return false;
×
1119
                        }
1120
                }
1121

1122
                /**
1123
                 * Get the set of jobs changed.
1124
                 *
1125
                 * @param timeout
1126
                 *            The timeout to wait for until something happens.
1127
                 * @return The set of changed job identifiers.
1128
                 */
1129
                @Override
1130
                public Collection<Integer> getChanged(Duration timeout) {
1131
                        if (isNull(epoch)) {
1✔
1132
                                return jobs.stream().map(Job::getId).collect(toSet());
1✔
1133
                        }
1134
                        try {
1135
                                return epoch.getChanged(timeout);
×
1136
                        } catch (InterruptedException interrupted) {
×
1137
                                currentThread().interrupt();
×
1138
                                return jobs.stream().map(Job::getId).collect(toSet());
×
1139
                        }
1140
                }
1141

1142
                @Override
1143
                public List<Job> jobs() {
1144
                        return copy(jobs);
×
1145
                }
1146

1147
                @Override
1148
                public List<Integer> ids() {
1149
                        return jobs.stream().map(Job::getId).collect(toList());
1✔
1150
                }
1151
        }
1152

1153
        private final class BoardReportSQL extends AbstractSQL {
1✔
1154
                final Query findBoardByChip = conn.query(findBoardByJobChip);
1✔
1155

1156
                final Query findBoardByTriad = conn.query(findBoardByLogicalCoords);
1✔
1157

1158
                final Query findBoardPhys = conn.query(findBoardByPhysicalCoords);
1✔
1159

1160
                final Query findBoardNet = conn.query(findBoardByIPAddress);
1✔
1161

1162
                final Update insertReport = conn.update(INSERT_BOARD_REPORT);
1✔
1163

1164
                final Query getReported = conn.query(GET_REPORTED_BOARDS);
1✔
1165

1166
                final Update setFunctioning = conn.update(SET_FUNCTIONING_FIELD);
1✔
1167

1168
                final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
1✔
1169

1170
                @Override
1171
                public void close() {
1172
                        findBoardByChip.close();
1✔
1173
                        findBoardByTriad.close();
1✔
1174
                        findBoardPhys.close();
1✔
1175
                        findBoardNet.close();
1✔
1176
                        insertReport.close();
1✔
1177
                        getReported.close();
1✔
1178
                        setFunctioning.close();
1✔
1179
                        getNamedMachine.close();
1✔
1180
                        super.close();
1✔
1181
                }
1✔
1182
        }
1183

1184
        /** Used to assemble an issue-report email for sending. */
1185
        private static final class EmailBuilder {
1186
                /**
1187
                 * More efficient than several String.format() calls, and much clearer
1188
                 * than a mess of direct {@link StringBuilder} calls!
1189
                 */
1190
                private final Formatter b = new Formatter(Locale.UK);
1✔
1191

1192
                private final int id;
1193

1194
                /**
1195
                 * @param id
1196
                 *            The job ID
1197
                 */
1198
                EmailBuilder(int id) {
1✔
1199
                        this.id = id;
1✔
1200
                }
1✔
1201

1202
                void header(String issue, int numBoards, String who) {
1203
                        b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
1✔
1204
                                        numBoards, who);
1✔
1205
                }
1✔
1206

1207
                void chip(ReportedBoard board) {
1208
                        b.format("\tBoard for job (%d) chip %s\n", //
×
1209
                                        id, board.chip());
×
1210
                }
×
1211

1212
                void triad(ReportedBoard board) {
1213
                        b.format("\tBoard for job (%d) board (X:%d,Y:%d,Z:%d)\n", //
×
1214
                                        id, board.x(), board.y(), board.z());
×
1215
                }
×
1216

1217
                void phys(ReportedBoard board) {
1218
                        b.format(
×
1219
                                        "\tBoard for job (%d) board "
1220
                                                        + "[Cabinet:%d,Frame:%d,Board:%d]\n", //
1221
                                        id, board.cabinet(), board.frame(), board.board());
×
1222
                }
×
1223

1224
                void ip(ReportedBoard board) {
1225
                        b.format("\tBoard for job (%d) board (IP: %s)\n", //
1✔
1226
                                        id, board.address());
1✔
1227
                }
1✔
1228

1229
                void issue(int issueId) {
1230
                        b.format("\t\tAction: noted as issue #%d\n", //
1✔
1231
                                        issueId);
1✔
1232
                }
1✔
1233

1234
                void footer(int numActions) {
1235
                        b.format("\nSummary: %d boards taken out of service.\n",
×
1236
                                        numActions);
×
1237
                }
×
1238

1239
                void serviceActionDone(Reported report) {
1240
                        b.format(
×
1241
                                        "\tAction: board (X:%d,Y:%d,Z:%d) (IP: %s) "
1242
                                                        + "taken out of service once not in use "
1243
                                                        + "(%d problems reported)\n",
1244
                                        report.x, report.y, report.z,
×
1245
                                        report.address, report.numReports);
×
1246
                }
×
1247

1248
                /** @return The assembled message body. */
1249
                @Override
1250
                public String toString() {
1251
                        return b.toString();
×
1252
                }
1253
        }
1254

1255
        private final class JobImpl implements Job {
1256
                @JsonIgnore
1257
                private Epoch epoch;
1258

1259
                private final int id;
1260

1261
                private final int machineId;
1262

1263
                private Integer width;
1264

1265
                private Integer height;
1266

1267
                private Integer depth;
1268

1269
                private JobState state;
1270

1271
                /** If not {@code null}, the ID of the root board of the job. */
1272
                private Integer root;
1273

1274
                private ChipLocation chipRoot;
1275

1276
                private String owner;
1277

1278
                private String keepaliveHost;
1279

1280
                private Instant startTime;
1281

1282
                private Instant keepaliveTime;
1283

1284
                private Instant finishTime;
1285

1286
                private String deathReason;
1287

1288
                private byte[] request;
1289

1290
                private boolean partial;
1291

1292
                private MachineImpl cachedMachine;
1293

1294
                JobImpl(int id, int machineId) {
1✔
1295
                        this.epoch = epochs.getJobsEpoch(id);
1✔
1296
                        this.id = id;
1✔
1297
                        this.machineId = machineId;
1✔
1298
                        partial = true;
1✔
1299
                }
1✔
1300

1301
                JobImpl(int jobId, int machineId, JobState jobState,
1302
                                Instant keepalive) {
1303
                        this(jobId, machineId);
1✔
1304
                        state = jobState;
1✔
1305
                        keepaliveTime = keepalive;
1✔
1306
                }
1✔
1307

1308
                JobImpl(Connection conn, Row row) {
1✔
1309
                        this.id = row.getInt("job_id");
1✔
1310
                        this.machineId = row.getInt("machine_id");
1✔
1311
                        width = row.getInteger("width");
1✔
1312
                        height = row.getInteger("height");
1✔
1313
                        depth = row.getInteger("depth");
1✔
1314
                        root = row.getInteger("root_id");
1✔
1315
                        owner = row.getString("owner");
1✔
1316
                        if (nonNull(root)) {
1✔
1317
                                try (var boardRoot = conn.query(GET_ROOT_OF_BOARD)) {
1✔
1318
                                        chipRoot = boardRoot.call1(chip("root_x", "root_y"), root)
1✔
1319
                                                        .orElse(null);
1✔
1320
                                }
1321
                        }
1322
                        state = row.getEnum("job_state", JobState.class);
1✔
1323
                        keepaliveHost = row.getString("keepalive_host");
1✔
1324
                        keepaliveTime = row.getInstant("keepalive_timestamp");
1✔
1325
                        startTime = row.getInstant("create_timestamp");
1✔
1326
                        finishTime = row.getInstant("death_timestamp");
1✔
1327
                        deathReason = row.getString("death_reason");
1✔
1328
                        request = row.getBytes("original_request");
1✔
1329
                        partial = false;
1✔
1330

1331
                        this.epoch = epochs.getJobsEpoch(id);
1✔
1332
                }
1✔
1333

1334
                /**
1335
                 * Get the machine that this job is running on. May used a cached value.
1336
                 * A transaction is required, but may be a read-only transaction.
1337
                 *
1338
                 * @param conn
1339
                 *            The connection to the DB
1340
                 * @return The overall machine handle.
1341
                 */
1342
                private synchronized MachineImpl getJobMachine(Connection conn) {
1343
                        if (cachedMachine == null || !cachedMachine.epoch.isValid()) {
1✔
1344
                                cachedMachine = Spalloc.this.getMachine(machineId, true, conn)
1✔
1345
                                                .orElseThrow();
1✔
1346
                        }
1347
                        return cachedMachine;
1✔
1348
                }
1349

1350
                @Override
1351
                public void access(String keepaliveAddress) {
1352
                        if (partial) {
1✔
1353
                                throw new PartialJobException();
×
1354
                        }
1355
                        try (var conn = getConnection();
1✔
1356
                                        var keepAlive = conn.update(UPDATE_KEEPALIVE)) {
1✔
1357
                                conn.transaction(() -> keepAlive.call(keepaliveAddress, id));
1✔
1358
                        }
1359
                }
1✔
1360

1361
                @Override
1362
                public void destroy(String reason) {
1363
                        if (partial) {
1✔
1364
                                throw new PartialJobException();
×
1365
                        }
1366
                        powerController.destroyJob(id, reason);
1✔
1367
                        rememberer.killProxies(id);
1✔
1368
                }
1✔
1369

1370
                @Override
1371
                public boolean waitForChange(Duration timeout) {
1372
                        if (isNull(epoch)) {
1✔
1373
                                return true;
×
1374
                        }
1375
                        try {
1376
                                return epoch.waitForChange(timeout);
×
1377
                        } catch (InterruptedException interrupted) {
1✔
1378
                                currentThread().interrupt();
1✔
1379
                                return false;
1✔
1380
                        }
1381
                }
1382

1383
                @Override
1384
                public int getId() {
1385
                        return id;
1✔
1386
                }
1387

1388
                @Override
1389
                public JobState getState() {
1390
                        return state;
1✔
1391
                }
1392

1393
                @Override
1394
                public Instant getStartTime() {
1395
                        return startTime;
1✔
1396
                }
1397

1398
                @Override
1399
                public Optional<Instant> getFinishTime() {
1400
                        return Optional.ofNullable(finishTime);
1✔
1401
                }
1402

1403
                @Override
1404
                public Optional<String> getReason() {
1405
                        return Optional.ofNullable(deathReason);
1✔
1406
                }
1407

1408
                @Override
1409
                public Optional<String> getKeepaliveHost() {
1410
                        if (partial) {
1✔
1411
                                return Optional.empty();
×
1412
                        }
1413
                        return Optional.ofNullable(keepaliveHost);
1✔
1414
                }
1415

1416
                @Override
1417
                public Instant getKeepaliveTimestamp() {
1418
                        return keepaliveTime;
1✔
1419
                }
1420

1421
                @Override
1422
                public Optional<byte[]> getOriginalRequest() {
1423
                        if (partial) {
1✔
1424
                                return Optional.empty();
×
1425
                        }
1426
                        return Optional.ofNullable(request);
1✔
1427
                }
1428

1429
                @Override
1430
                public Optional<SubMachine> getMachine() {
1431
                        if (isNull(root)) {
1✔
1432
                                return Optional.empty();
1✔
1433
                        }
1434
                        return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
1✔
1435
                }
1436

1437
                @Override
1438
                public Optional<BoardLocation> whereIs(int x, int y) {
1439
                        if (isNull(root)) {
1✔
1440
                                return Optional.empty();
×
1441
                        }
1442
                        try (var conn = getConnection();
1✔
1443
                                        var findBoard = conn.query(findBoardByJobChip)) {
1✔
1444
                                return conn.transaction(false, () -> findBoard
1✔
1445
                                                .call1(row -> new BoardLocationImpl(row,
1✔
1446
                                                                getJobMachine(conn)), id, root, x, y));
1✔
1447
                        }
1448
                }
1449

1450
                // -------------------------------------------------------------
1451
                // Bad board report handling
1452

1453
                @Override
1454
                public String reportIssue(IssueReportRequest report, Permit permit) {
1455
                        try (var q = new BoardReportSQL()) {
1✔
1456
                                var email = new EmailBuilder(id);
1✔
1457
                                var result = q.transaction(
1✔
1458
                                                () -> reportIssue(report, permit, email, q));
1✔
1459
                                emailSender.sendServiceMail(email);
1✔
1460
                                for (var m : report.boards().stream()
1✔
1461
                                                .map(b -> q.getNamedMachine.call1(
1✔
1462
                                                                r -> r.getInt("machine_id"), b.machine(), true))
1✔
1463
                                                .collect(toSet())) {
1✔
1464
                                        if (m.isPresent()) {
1✔
1465
                                                epochs.machineChanged(m.get());
×
1466
                                        }
1467
                                }
1✔
1468

1469
                                return result;
1✔
1470
                        } catch (ReportRollbackExn e) {
×
1471
                                return e.getMessage();
×
1472
                        }
1473
                }
1474

1475
                /**
1476
                 * Report an issue with some boards and assemble the email to send. This
1477
                 * may result in boards being taken out of service (i.e., no longer
1478
                 * being available to be allocated; their current allocation will
1479
                 * continue).
1480
                 * <p>
1481
                 * <strong>NB:</strong> The sending of the email sending is
1482
                 * <em>outside</em> the transaction that this code is executed in.
1483
                 *
1484
                 * @param report
1485
                 *            The report from the user.
1486
                 * @param permit
1487
                 *            Who the user is.
1488
                 * @param email
1489
                 *            The email we're assembling.
1490
                 * @param q
1491
                 *            SQL access queries.
1492
                 * @return Summary of action taken message, to go to user.
1493
                 * @throws ReportRollbackExn
1494
                 *             If the report is bad somehow.
1495
                 */
1496
                private String reportIssue(IssueReportRequest report, Permit permit,
1497
                                EmailBuilder email, BoardReportSQL q) throws ReportRollbackExn {
1498
                        email.header(report.issue(), report.boards().size(), permit.name);
1✔
1499
                        int userId = getUser(q.getConnection(), permit.name)
1✔
1500
                                        .orElseThrow(() -> new ReportRollbackExn(
1✔
1501
                                                        "no such user: %s", permit.name));
1502
                        for (var board : report.boards()) {
1✔
1503
                                addIssueReport(q, getJobBoardForReport(q, board, email),
1✔
1504
                                                report.issue(), userId, email);
1✔
1505
                        }
1✔
1506
                        return takeBoardsOutOfService(q, email).map(acted -> {
1✔
1507
                                email.footer(acted);
×
1508
                                return format("%d boards taken out of service", acted);
×
1509
                        }).orElse("report noted");
1✔
1510
                }
1511

1512
                /**
1513
                 * Convert a board locator (for an issue report) into a board ID.
1514
                 *
1515
                 * @param q
1516
                 *            How to touch the DB
1517
                 * @param board
1518
                 *            What board are we talking about
1519
                 * @param email
1520
                 *            The email we are building.
1521
                 * @return The board ID
1522
                 * @throws ReportRollbackExn
1523
                 *             If the board can't be converted to an ID
1524
                 */
1525
                private int getJobBoardForReport(BoardReportSQL q, ReportedBoard board,
1526
                                EmailBuilder email) throws ReportRollbackExn {
1527
                        Problem r;
1528
                        if (nonNull(board.chip())) {
1✔
1529
                                r = q.findBoardByChip
×
1530
                                                .call1(Problem::new, id, root, board.chip().getX(),
×
1531
                                                                board.chip().getY())
×
1532
                                                .orElseThrow(() -> new ReportRollbackExn(board.chip()));
×
1533
                                email.chip(board);
×
1534
                        } else if (nonNull(board.x())) {
1✔
1535
                                r = q.findBoardByTriad
×
1536
                                                .call1(Problem::new, machineId, board.x(), board.y(),
×
1537
                                                                board.z())
×
1538
                                                .orElseThrow(() -> new ReportRollbackExn(
×
1539
                                                                "triad (%s,%s,%s) not in machine", board.x(),
×
1540
                                                                board.y(), board.z()));
×
1541
                                if (isNull(r.jobId) || id != r.jobId) {
×
1542
                                        throw new ReportRollbackExn(
×
1543
                                                        "triad (%s,%s,%s) not allocated to job %d",
1544
                                                        board.x(), board.y(), board.z(), id);
×
1545
                                }
1546
                                email.triad(board);
×
1547
                        } else if (nonNull(board.cabinet())) {
1✔
1548
                                r = q.findBoardPhys
×
1549
                                                .call1(Problem::new, machineId, board.cabinet(),
×
1550
                                                                board.frame(), board.board())
×
1551
                                                .orElseThrow(() -> new ReportRollbackExn(
×
1552
                                                                "physical board [%s,%s,%s] not in machine",
1553
                                                                board.cabinet(), board.frame(), board.board()));
×
1554
                                if (isNull(r.jobId) || id != r.jobId) {
×
1555
                                        throw new ReportRollbackExn(
×
1556
                                                        "physical board [%s,%s,%s] not allocated to job %d",
1557
                                                        board.cabinet(), board.frame(), board.board(), id);
×
1558
                                }
1559
                                email.phys(board);
×
1560
                        } else if (nonNull(board.address())) {
1✔
1561
                                r = q.findBoardNet
1✔
1562
                                                .call1(Problem::new, machineId, board.address())
1✔
1563
                                                .orElseThrow(() -> new ReportRollbackExn(
1✔
1564
                                                                "board at %s not in machine", board.address()));
×
1565
                                if (isNull(r.jobId) || id != r.jobId) {
1✔
1566
                                        throw new ReportRollbackExn(
×
1567
                                                        "board at %s not allocated to job %d",
1568
                                                        board.address(), id);
×
1569
                                }
1570
                                email.ip(board);
1✔
1571
                        } else {
1572
                                throw new UnsupportedOperationException();
×
1573
                        }
1574
                        return r.boardId;
1✔
1575
                }
1576

1577
                /**
1578
                 * Record a reported issue with a board.
1579
                 *
1580
                 * @param u
1581
                 *            How to touch the DB
1582
                 * @param boardId
1583
                 *            What board has the issue?
1584
                 * @param issue
1585
                 *            What is the issue?
1586
                 * @param userId
1587
                 *            Who is doing the report?
1588
                 * @param email
1589
                 *            The email we are building.
1590
                 */
1591
                private void addIssueReport(BoardReportSQL u, int boardId, String issue,
1592
                                int userId, EmailBuilder email) {
1593
                        u.insertReport.key(boardId, id, issue, userId)
1✔
1594
                                        .ifPresent(email::issue);
1✔
1595
                }
1✔
1596

1597
                // -------------------------------------------------------------
1598

1599
                @Override
1600
                public Optional<ChipLocation> getRootChip() {
1601
                        return Optional.ofNullable(chipRoot);
1✔
1602
                }
1603

1604
                @Override
1605
                public Optional<String> getOwner() {
1606
                        if (partial) {
1✔
1607
                                return Optional.empty();
×
1608
                        }
1609
                        return Optional.ofNullable(owner);
1✔
1610
                }
1611

1612
                @Override
1613
                public Optional<Integer> getWidth() {
1614
                        return Optional.ofNullable(width);
1✔
1615
                }
1616

1617
                @Override
1618
                public Optional<Integer> getHeight() {
1619
                        return Optional.ofNullable(height);
1✔
1620
                }
1621

1622
                @Override
1623
                public Optional<Integer> getDepth() {
1624
                        return Optional.ofNullable(depth);
1✔
1625
                }
1626

1627
                @Override
1628
                public void rememberProxy(ProxyCore proxy) {
1629
                        rememberer.rememberProxyForJob(id, proxy);
×
1630
                }
×
1631

1632
                @Override
1633
                public void forgetProxy(ProxyCore proxy) {
1634
                        rememberer.removeProxyForJob(id, proxy);
×
1635
                }
×
1636

1637
                @Override
1638
                public boolean equals(Object other) {
1639
                        // Equality is defined exactly by the database ID
1640
                        return (other instanceof JobImpl j) && (id == j.id);
×
1641
                }
1642

1643
                @Override
1644
                public int hashCode() {
1645
                        return id;
×
1646
                }
1647

1648
                @Override
1649
                public String toString() {
1650
                        return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1651
                                        width, height, depth, startTime, finishTime);
1652
                }
1653

1654
                private final class SubMachineImpl implements SubMachine {
1655
                        /** The machine that this sub-machine is part of. */
1656
                        private final Machine machine;
1657

1658
                        /** The root X coordinate of this sub-machine. */
1659
                        private int rootX;
1660

1661
                        /** The root Y coordinate of this sub-machine. */
1662
                        private int rootY;
1663

1664
                        /** The root Z coordinate of this sub-machine. */
1665
                        private int rootZ;
1666

1667
                        /** The connection details of this sub-machine. */
1668
                        private List<ConnectionInfo> connections;
1669

1670
                        /** The board locations of this sub-machine. */
1671
                        private List<BoardCoordinates> boards;
1672

1673
                        private List<Integer> boardIds;
1674

1675
                        private SubMachineImpl(Connection conn) {
1✔
1676
                                machine = getJobMachine(conn);
1✔
1677
                                try (var getRootXY = conn.query(GET_ROOT_COORDS);
1✔
1678
                                                var getBoardInfo = conn.query(GET_BOARD_CONNECT_INFO)) {
1✔
1679
                                        getRootXY.call1(row -> {
1✔
1680
                                                rootX = row.getInt("x");
1✔
1681
                                                rootY = row.getInt("y");
1✔
1682
                                                rootZ = row.getInt("z");
1✔
1683
                                                // We have to return something,
1684
                                                // but it doesn't matter what
1685
                                                return true;
1✔
1686
                                        }, root);
1687
                                        int capacityEstimate = width * height;
1✔
1688
                                        connections = new ArrayList<>(capacityEstimate);
1✔
1689
                                        boards = new ArrayList<>(capacityEstimate);
1✔
1690
                                        boardIds = new ArrayList<>(capacityEstimate);
1✔
1691
                                        getBoardInfo.call(row -> {
1✔
1692
                                                boardIds.add(row.getInt("board_id"));
1✔
1693
                                                boards.add(new BoardCoordinates(row.getInt("x"),
1✔
1694
                                                                row.getInt("y"), row.getInt("z")));
1✔
1695
                                                connections.add(new ConnectionInfo(
1✔
1696
                                                                relativeChipLocation(row.getInt("root_x"),
1✔
1697
                                                                                row.getInt("root_y")),
1✔
1698
                                                                row.getString("address")));
1✔
1699
                                                // We have to return something,
1700
                                                // but it doesn't matter what
1701
                                                return true;
1✔
1702
                                        }, id);
1✔
1703
                                }
1704
                        }
1✔
1705

1706
                        private ChipLocation relativeChipLocation(int x, int y) {
1707
                                x -= chipRoot.getX();
1✔
1708
                                y -= chipRoot.getY();
1✔
1709
                                // Allow for wrapping
1710
                                if (x < 0) {
1✔
1711
                                        x += machine.getWidth() * TRIAD_CHIP_SIZE;
×
1712
                                }
1713
                                if (y < 0) {
1✔
1714
                                        y += machine.getHeight() * TRIAD_CHIP_SIZE;
×
1715
                                }
1716
                                return new ChipLocation(x, y);
1✔
1717
                        }
1718

1719
                        @Override
1720
                        public Machine getMachine() {
1721
                                return machine;
1✔
1722
                        }
1723

1724
                        @Override
1725
                        public int getRootX() {
1726
                                return rootX;
1✔
1727
                        }
1728

1729
                        @Override
1730
                        public int getRootY() {
1731
                                return rootY;
1✔
1732
                        }
1733

1734
                        @Override
1735
                        public int getRootZ() {
1736
                                return rootZ;
1✔
1737
                        }
1738

1739
                        @Override
1740
                        public int getWidth() {
1741
                                return width;
1✔
1742
                        }
1743

1744
                        @Override
1745
                        public int getHeight() {
1746
                                return height;
1✔
1747
                        }
1748

1749
                        @Override
1750
                        public int getDepth() {
1751
                                return depth;
1✔
1752
                        }
1753

1754
                        @Override
1755
                        public List<ConnectionInfo> getConnections() {
1756
                                return connections;
1✔
1757
                        }
1758

1759
                        @Override
1760
                        public List<BoardCoordinates> getBoards() {
1761
                                return boards;
1✔
1762
                        }
1763

1764
                        @Override
1765
                        public PowerState getPower() {
1766
                                try (var conn = getConnection();
1✔
1767
                                                var power = conn.query(GET_SUM_BOARDS_POWERED)) {
1✔
1768
                                        return conn.transaction(false,
1✔
1769
                                                        () -> power.call1(integer("total_on"), id)
1✔
1770
                                                                        .map(totalOn -> totalOn < boardIds.size()
1✔
1771
                                                                                        ? OFF
1✔
1772
                                                                                        : ON)
×
1773
                                                                        .orElse(null));
1✔
1774
                                }
1775
                        }
1776

1777
                        @Override
1778
                        public void setPower(PowerState ps) {
1779
                                if (partial) {
1✔
1780
                                        throw new PartialJobException();
×
1781
                                }
1782
                                powerController.setPower(id, ps, READY);
1✔
1783
                        }
1✔
1784
                }
1785
        }
1786

1787
        /**
1788
         * Board location implementation. Does not retain database connections after
1789
         * creation.
1790
         *
1791
         * @author Donal Fellows
1792
         */
1793
        private final class BoardLocationImpl implements BoardLocation {
1794
                private JobImpl job;
1795

1796
                private final String machineName;
1797

1798
                private final int machineWidth;
1799

1800
                private final int machineHeight;
1801

1802
                private final ChipLocation chip;
1803

1804
                private final ChipLocation boardChip;
1805

1806
                private final BoardCoordinates logical;
1807

1808
                private final BoardPhysicalCoordinates physical;
1809

1810
                // Transaction is open
1811
                private BoardLocationImpl(Row row, Machine machine) {
1✔
1812
                        machineName = row.getString("machine_name");
1✔
1813
                        logical = new BoardCoordinates(row.getInt("x"), row.getInt("y"),
1✔
1814
                                        row.getInt("z"));
1✔
1815
                        physical = new BoardPhysicalCoordinates(row.getInt("cabinet"),
1✔
1816
                                        row.getInt("frame"), row.getInteger("board_num"));
1✔
1817
                        chip = row.getChip("chip_x", "chip_y");
1✔
1818
                        machineWidth = machine.getWidth();
1✔
1819
                        machineHeight = machine.getHeight();
1✔
1820
                        var boardX = row.getInteger("board_chip_x");
1✔
1821
                        if (nonNull(boardX)) {
1✔
1822
                                boardChip = row.getChip("board_chip_x", "board_chip_y");
1✔
1823
                        } else {
1824
                                boardChip = chip;
×
1825
                        }
1826

1827
                        var jobId = row.getInteger("job_id");
1✔
1828
                        if (nonNull(jobId)) {
1✔
1829
                                job = new JobImpl(jobId, machine.getId());
1✔
1830
                                job.chipRoot = row.getChip("job_root_chip_x",
1✔
1831
                                                "job_root_chip_y");
1832
                        }
1833
                }
1✔
1834

1835
                @Override
1836
                public ChipLocation getBoardChip() {
1837
                        return boardChip;
1✔
1838
                }
1839

1840
                @Override
1841
                public ChipLocation getChipRelativeTo(ChipLocation rootChip) {
1842
                        int x = chip.getX() - rootChip.getX();
1✔
1843
                        if (x < 0) {
1✔
1844
                                x += machineWidth * TRIAD_CHIP_SIZE;
×
1845
                        }
1846
                        int y = chip.getY() - rootChip.getY();
1✔
1847
                        if (y < 0) {
1✔
1848
                                y += machineHeight * TRIAD_CHIP_SIZE;
×
1849
                        }
1850
                        return new ChipLocation(x, y);
1✔
1851
                }
1852

1853
                @Override
1854
                public String getMachine() {
1855
                        return machineName;
1✔
1856
                }
1857

1858
                @Override
1859
                public BoardCoordinates getLogical() {
1860
                        return logical;
1✔
1861
                }
1862

1863
                @Override
1864
                public BoardPhysicalCoordinates getPhysical() {
1865
                        return physical;
1✔
1866
                }
1867

1868
                @Override
1869
                public ChipLocation getChip() {
1870
                        return chip;
1✔
1871
                }
1872

1873
                @Override
1874
                public Job getJob() {
1875
                        return job;
1✔
1876
                }
1877
        }
1878

1879
        static class PartialJobException extends IllegalStateException {
1880
                @Serial
1881
                private static final long serialVersionUID = 2997856394666135483L;
1882

1883
                PartialJobException() {
1884
                        super("partial job only");
×
1885
                }
×
1886
        }
1887
}
1888

1889
class ReportRollbackExn extends RuntimeException {
1890
        private static final long serialVersionUID = 1L;
1891

1892
        @FormatMethod
1893
        ReportRollbackExn(String msg, Object... args) {
1894
                super(format(msg, args));
×
1895
        }
×
1896

1897
        ReportRollbackExn(HasChipLocation chip) {
1898
                this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
1899
                                chip.getY());
×
1900
        }
×
1901
}
1902

1903
abstract class GroupsException extends RuntimeException {
1904
        @Serial
1905
        private static final long serialVersionUID = 6607077117924279611L;
1906

1907
        GroupsException(String message) {
1908
                super(message);
×
1909
        }
×
1910

1911
        GroupsException(String message, Throwable cause) {
1912
                super(message, cause);
×
1913
        }
×
1914
}
1915

1916
class NoSuchGroupException extends GroupsException {
1917
        @Serial
1918
        private static final long serialVersionUID = 5193818294198205503L;
1919

1920
        @FormatMethod
1921
        NoSuchGroupException(String msg, Object... args) {
1922
                super(format(msg, args));
×
1923
        }
×
1924
}
1925

1926
class MultipleGroupsException extends GroupsException {
1927
        @Serial
1928
        private static final long serialVersionUID = 6284332340565334236L;
1929

1930
        @FormatMethod
1931
        MultipleGroupsException(String msg, Object... args) {
1932
                super(format(msg, args));
×
1933
        }
×
1934
}
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