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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

19 Sep 2023 08:46AM UTC coverage: 36.409% (-0.6%) from 36.982%
6233274834

Pull #658

github

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

1656 of 1656 new or added lines in 260 files covered. (100.0%)

8373 of 22997 relevant lines covered (36.41%)

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.alloc.security.SecurityConfig.MAY_SEE_JOB_DETAILS;
36
import static uk.ac.manchester.spinnaker.utils.CollectionUtils.copy;
37
import static uk.ac.manchester.spinnaker.utils.OptionalUtils.apply;
38

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

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

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

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

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

104
        private static final Logger log = getLogger(Spalloc.class);
1✔
105

106
        @Autowired
107
        private PowerController powerController;
108

109
        @Autowired
110
        private Epochs epochs;
111

112
        @Autowired
113
        private QuotaManager quotaManager;
114

115
        @Autowired
116
        private ReportMailSender emailSender;
117

118
        @Autowired
119
        private AllocatorProperties props;
120

121
        @Autowired
122
        private ProxyRememberer rememberer;
123

124
        @Autowired
125
        private AllocatorTask allocator;
126

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

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

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

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

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

152
                private final Query countMachineThings =
1✔
153
                                conn.query(COUNT_MACHINE_THINGS);
1✔
154

155
                private final Query getTags = conn.query(GET_TAGS);
1✔
156

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

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

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

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

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

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

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

221
                final Query countMachineThings = conn.query(COUNT_MACHINE_THINGS);
1✔
222

223
                final Query getTags = conn.query(GET_TAGS);
1✔
224

225
                final Query getJobs = conn.query(GET_MACHINE_JOBS);
1✔
226

227
                final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
1✔
228

229
                final Query getLive = conn.query(GET_LIVE_BOARDS);
1✔
230

231
                final Query getDead = conn.query(GET_DEAD_BOARDS);
1✔
232

233
                final Query getQuota = conn.query(GET_USER_QUOTA);
1✔
234

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

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

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

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

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

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

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

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

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

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

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

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

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

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

432
                        var m = selectMachine(conn, machineName, tags);
1✔
433
                        if (!m.isPresent()) {
1✔
434
                                // Cannot find machine!
435
                                return Optional.empty();
×
436
                        }
437
                        var machine = m.orElseThrow();
1✔
438

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

447
                        var scale = props.getPriorityScale();
1✔
448

449
                        if (machine.getArea() < descriptor.getArea()) {
1✔
450
                                throw new IllegalArgumentException(
×
451
                                                "request cannot fit on machine");
452
                        }
453

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

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

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

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

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

515
                        allocator.scheduleAllocateNow();
1✔
516
                        return getJob(jobId, conn).map(ji -> (Job) ji);
1✔
517
                });
518
        }
519

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

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

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

549
                quotaManager.associateNMPISession(
×
550
                                job.get().getId(), owner, nmpiCollab, quotaUnits.get());
×
551

552
                // Return the job created
553
                return job;
×
554
        }
555

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

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

575
                quotaManager.associateNMPIJob(job.get().getId(), nmpiJobId,
×
576
                                quotaDetails.quotaUnits());
×
577

578
                // Return the job created
579
                return job;
×
580
        }
581

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

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

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

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

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

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

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

709
        @Override
710
        public void purgeDownCache() {
711
                synchronized (this) {
×
712
                        downBoardsCache.clear();
×
713
                        downLinksCache.clear();
×
714
                }
×
715
        }
×
716

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

732
        private record Problem(int boardId, Integer jobId) {
1✔
733
                Problem(Row row) {
734
                        this(row.getInt("board_id"), row.getInt("job_id"));
1✔
735
                }
1✔
736
        }
737

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

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

777
        private record Reported(int boardId, int x, int y, int z, String address,
×
778
                        int numReports) {
779
                Reported(Row row) {
780
                        this(row.getInt("board_id"), row.getInt("x"), row.getInt("y"),
×
781
                                        row.getInt("z"), row.getString("address"),
×
782
                                        row.getInt("numReports"));
×
783
                }
×
784
        }
785

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

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

825
        private final class MachineImpl implements Machine {
826
                private final int id;
827

828
                private final boolean inService;
829

830
                private final String name;
831

832
                private final Set<String> tags;
833

834
                private final int width;
835

836
                private final int height;
837

838
                private boolean lookedUpWraps;
839

840
                private boolean hWrap;
841

842
                private boolean vWrap;
843

844
                @JsonIgnore
845
                private final Epoch epoch;
846

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

859
                        this.epoch = epochs.getMachineEpoch(id);
1✔
860
                }
1✔
861

862
                private int getArea() {
863
                        return width * height * TRIAD_DEPTH;
1✔
864
                }
865

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

881
                private BoardLocation boardLoc(Row row) {
882
                        return new BoardLocationImpl(row, this);
1✔
883
                }
884

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

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

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

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

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

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

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

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

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

996
                @Override
997
                public int getId() {
998
                        return id;
1✔
999
                }
1000

1001
                @Override
1002
                public String getName() {
1003
                        return name;
1✔
1004
                }
1005

1006
                @Override
1007
                public Set<String> getTags() {
1008
                        return tags;
1✔
1009
                }
1010

1011
                @Override
1012
                public int getWidth() {
1013
                        return width;
1✔
1014
                }
1015

1016
                @Override
1017
                public int getHeight() {
1018
                        return height;
1✔
1019
                }
1020

1021
                @Override
1022
                public boolean isInService() {
1023
                        return inService;
1✔
1024
                }
1025

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

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

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

1054
                @Override
1055
                public int hashCode() {
1056
                        return id;
×
1057
                }
1058

1059
                @Override
1060
                public String toString() {
1061
                        return "Machine(" + name + ")";
×
1062
                }
1063

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

1080
                @Override
1081
                public boolean isHorizonallyWrapped() {
1082
                        if (!lookedUpWraps) {
×
1083
                                retrieveWraps();
×
1084
                        }
1085
                        return hWrap;
×
1086
                }
1087

1088
                @Override
1089
                public boolean isVerticallyWrapped() {
1090
                        if (!lookedUpWraps) {
×
1091
                                retrieveWraps();
×
1092
                        }
1093
                        return vWrap;
×
1094
                }
1095
        }
1096

1097
        private final class JobCollection implements Jobs {
1098
                @JsonIgnore
1099
                private final Epoch epoch;
1100

1101
                private final List<Job> jobs;
1102

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

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

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

1146
                @Override
1147
                public List<Job> jobs() {
1148
                        return copy(jobs);
×
1149
                }
1150

1151
                @Override
1152
                public List<Integer> ids() {
1153
                        return jobs.stream().map(Job::getId).collect(toList());
1✔
1154
                }
1155
        }
1156

1157
        private final class BoardReportSQL extends AbstractSQL {
1✔
1158
                final Query findBoardByChip = conn.query(findBoardByJobChip);
1✔
1159

1160
                final Query findBoardByTriad = conn.query(findBoardByLogicalCoords);
1✔
1161

1162
                final Query findBoardPhys = conn.query(findBoardByPhysicalCoords);
1✔
1163

1164
                final Query findBoardNet = conn.query(findBoardByIPAddress);
1✔
1165

1166
                final Update insertReport = conn.update(INSERT_BOARD_REPORT);
1✔
1167

1168
                final Query getReported = conn.query(GET_REPORTED_BOARDS);
1✔
1169

1170
                final Update setFunctioning = conn.update(SET_FUNCTIONING_FIELD);
1✔
1171

1172
                final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
1✔
1173

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

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

1196
                private final int id;
1197

1198
                /**
1199
                 * @param id
1200
                 *            The job ID
1201
                 */
1202
                EmailBuilder(int id) {
1✔
1203
                        this.id = id;
1✔
1204
                }
1✔
1205

1206
                void header(String issue, int numBoards, String who) {
1207
                        b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
1✔
1208
                                        numBoards, who);
1✔
1209
                }
1✔
1210

1211
                void chip(ReportedBoard board) {
1212
                        b.format("\tBoard for job (%d) chip %s\n", //
×
1213
                                        id, board.chip());
×
1214
                }
×
1215

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

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

1228
                void ip(ReportedBoard board) {
1229
                        b.format("\tBoard for job (%d) board (IP: %s)\n", //
1✔
1230
                                        id, board.address());
1✔
1231
                }
1✔
1232

1233
                void issue(int issueId) {
1234
                        b.format("\t\tAction: noted as issue #%d\n", //
1✔
1235
                                        issueId);
1✔
1236
                }
1✔
1237

1238
                void footer(int numActions) {
1239
                        b.format("\nSummary: %d boards taken out of service.\n",
×
1240
                                        numActions);
×
1241
                }
×
1242

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

1252
                /** @return The assembled message body. */
1253
                @Override
1254
                public String toString() {
1255
                        return b.toString();
×
1256
                }
1257
        }
1258

1259
        private final class JobImpl implements Job {
1260
                @JsonIgnore
1261
                private Epoch epoch;
1262

1263
                private final int id;
1264

1265
                private final int machineId;
1266

1267
                private Integer width;
1268

1269
                private Integer height;
1270

1271
                private Integer depth;
1272

1273
                private JobState state;
1274

1275
                /** If not {@code null}, the ID of the root board of the job. */
1276
                private Integer root;
1277

1278
                private ChipLocation chipRoot;
1279

1280
                private String owner;
1281

1282
                private String keepaliveHost;
1283

1284
                private Instant startTime;
1285

1286
                private Instant keepaliveTime;
1287

1288
                private Instant finishTime;
1289

1290
                private String deathReason;
1291

1292
                private byte[] request;
1293

1294
                private boolean partial;
1295

1296
                private MachineImpl cachedMachine;
1297

1298
                JobImpl(int id, int machineId) {
1✔
1299
                        this.epoch = epochs.getJobsEpoch(id);
1✔
1300
                        this.id = id;
1✔
1301
                        this.machineId = machineId;
1✔
1302
                        partial = true;
1✔
1303
                }
1✔
1304

1305
                JobImpl(int jobId, int machineId, JobState jobState,
1306
                                Instant keepalive) {
1307
                        this(jobId, machineId);
1✔
1308
                        state = jobState;
1✔
1309
                        keepaliveTime = keepalive;
1✔
1310
                }
1✔
1311

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

1335
                        this.epoch = epochs.getJobsEpoch(id);
1✔
1336
                }
1✔
1337

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

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

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

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

1387
                @Override
1388
                public int getId() {
1389
                        return id;
1✔
1390
                }
1391

1392
                @Override
1393
                public JobState getState() {
1394
                        return state;
1✔
1395
                }
1396

1397
                @Override
1398
                public Instant getStartTime() {
1399
                        return startTime;
1✔
1400
                }
1401

1402
                @Override
1403
                public Optional<Instant> getFinishTime() {
1404
                        return Optional.ofNullable(finishTime);
1✔
1405
                }
1406

1407
                @Override
1408
                public Optional<String> getReason() {
1409
                        return Optional.ofNullable(deathReason);
1✔
1410
                }
1411

1412
                @Override
1413
                public Optional<String> getKeepaliveHost() {
1414
                        if (partial) {
1✔
1415
                                return Optional.empty();
×
1416
                        }
1417
                        return Optional.ofNullable(keepaliveHost);
1✔
1418
                }
1419

1420
                @Override
1421
                public Instant getKeepaliveTimestamp() {
1422
                        return keepaliveTime;
1✔
1423
                }
1424

1425
                @Override
1426
                public Optional<byte[]> getOriginalRequest() {
1427
                        if (partial) {
1✔
1428
                                return Optional.empty();
×
1429
                        }
1430
                        return Optional.ofNullable(request);
1✔
1431
                }
1432

1433
                @Override
1434
                public Optional<SubMachine> getMachine() {
1435
                        if (isNull(root)) {
1✔
1436
                                return Optional.empty();
1✔
1437
                        }
1438
                        return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
1✔
1439
                }
1440

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

1454
                // -------------------------------------------------------------
1455
                // Bad board report handling
1456

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

1473
                                return result;
1✔
1474
                        } catch (ReportRollbackExn e) {
×
1475
                                return e.getMessage();
×
1476
                        }
1477
                }
1478

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

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

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

1601
                // -------------------------------------------------------------
1602

1603
                @Override
1604
                public Optional<ChipLocation> getRootChip() {
1605
                        return Optional.ofNullable(chipRoot);
1✔
1606
                }
1607

1608
                @Override
1609
                public Optional<String> getOwner() {
1610
                        if (partial) {
1✔
1611
                                return Optional.empty();
×
1612
                        }
1613
                        return Optional.ofNullable(owner);
1✔
1614
                }
1615

1616
                @Override
1617
                public Optional<Integer> getWidth() {
1618
                        return Optional.ofNullable(width);
1✔
1619
                }
1620

1621
                @Override
1622
                public Optional<Integer> getHeight() {
1623
                        return Optional.ofNullable(height);
1✔
1624
                }
1625

1626
                @Override
1627
                public Optional<Integer> getDepth() {
1628
                        return Optional.ofNullable(depth);
1✔
1629
                }
1630

1631
                @Override
1632
                public void rememberProxy(ProxyCore proxy) {
1633
                        rememberer.rememberProxyForJob(id, proxy);
×
1634
                }
×
1635

1636
                @Override
1637
                public void forgetProxy(ProxyCore proxy) {
1638
                        rememberer.removeProxyForJob(id, proxy);
×
1639
                }
×
1640

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

1647
                @Override
1648
                public int hashCode() {
1649
                        return id;
×
1650
                }
1651

1652
                @Override
1653
                public String toString() {
1654
                        return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1655
                                        width, height, depth, startTime, finishTime);
1656
                }
1657

1658
                private final class SubMachineImpl implements SubMachine {
1659
                        /** The machine that this sub-machine is part of. */
1660
                        private final Machine machine;
1661

1662
                        /** The root X coordinate of this sub-machine. */
1663
                        private int rootX;
1664

1665
                        /** The root Y coordinate of this sub-machine. */
1666
                        private int rootY;
1667

1668
                        /** The root Z coordinate of this sub-machine. */
1669
                        private int rootZ;
1670

1671
                        /** The connection details of this sub-machine. */
1672
                        private List<ConnectionInfo> connections;
1673

1674
                        /** The board locations of this sub-machine. */
1675
                        private List<BoardCoordinates> boards;
1676

1677
                        private List<Integer> boardIds;
1678

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

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

1723
                        @Override
1724
                        public Machine getMachine() {
1725
                                return machine;
1✔
1726
                        }
1727

1728
                        @Override
1729
                        public int getRootX() {
1730
                                return rootX;
1✔
1731
                        }
1732

1733
                        @Override
1734
                        public int getRootY() {
1735
                                return rootY;
1✔
1736
                        }
1737

1738
                        @Override
1739
                        public int getRootZ() {
1740
                                return rootZ;
1✔
1741
                        }
1742

1743
                        @Override
1744
                        public int getWidth() {
1745
                                return width;
1✔
1746
                        }
1747

1748
                        @Override
1749
                        public int getHeight() {
1750
                                return height;
1✔
1751
                        }
1752

1753
                        @Override
1754
                        public int getDepth() {
1755
                                return depth;
1✔
1756
                        }
1757

1758
                        @Override
1759
                        public List<ConnectionInfo> getConnections() {
1760
                                return connections;
1✔
1761
                        }
1762

1763
                        @Override
1764
                        public List<BoardCoordinates> getBoards() {
1765
                                return boards;
1✔
1766
                        }
1767

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

1781
                        @Override
1782
                        public void setPower(PowerState ps) {
1783
                                if (partial) {
1✔
1784
                                        throw new PartialJobException();
×
1785
                                }
1786
                                powerController.setPower(id, ps, READY);
1✔
1787
                        }
1✔
1788
                }
1789
        }
1790

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

1800
                private final String machineName;
1801

1802
                private final int machineWidth;
1803

1804
                private final int machineHeight;
1805

1806
                private final ChipLocation chip;
1807

1808
                private final ChipLocation boardChip;
1809

1810
                private final BoardCoordinates logical;
1811

1812
                private final BoardPhysicalCoordinates physical;
1813

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

1831
                        var jobId = row.getInteger("job_id");
1✔
1832
                        if (nonNull(jobId)) {
1✔
1833
                                job = new JobImpl(jobId, machine.getId());
1✔
1834
                                job.chipRoot = row.getChip("job_root_chip_x",
1✔
1835
                                                "job_root_chip_y");
1836
                        }
1837
                }
1✔
1838

1839
                @Override
1840
                public ChipLocation getBoardChip() {
1841
                        return boardChip;
1✔
1842
                }
1843

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

1857
                @Override
1858
                public String getMachine() {
1859
                        return machineName;
1✔
1860
                }
1861

1862
                @Override
1863
                public BoardCoordinates getLogical() {
1864
                        return logical;
1✔
1865
                }
1866

1867
                @Override
1868
                public BoardPhysicalCoordinates getPhysical() {
1869
                        return physical;
1✔
1870
                }
1871

1872
                @Override
1873
                public ChipLocation getChip() {
1874
                        return chip;
1✔
1875
                }
1876

1877
                @Override
1878
                public Job getJob() {
1879
                        return job;
1✔
1880
                }
1881
        }
1882

1883
        static class PartialJobException extends IllegalStateException {
1884
                @Serial
1885
                private static final long serialVersionUID = 2997856394666135483L;
1886

1887
                PartialJobException() {
1888
                        super("partial job only");
×
1889
                }
×
1890
        }
1891
}
1892

1893
class ReportRollbackExn extends RuntimeException {
1894
        private static final long serialVersionUID = 1L;
1895

1896
        @FormatMethod
1897
        ReportRollbackExn(String msg, Object... args) {
1898
                super(format(msg, args));
×
1899
        }
×
1900

1901
        ReportRollbackExn(HasChipLocation chip) {
1902
                this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
1903
                                chip.getY());
×
1904
        }
×
1905
}
1906

1907
abstract class GroupsException extends RuntimeException {
1908
        @Serial
1909
        private static final long serialVersionUID = 6607077117924279611L;
1910

1911
        GroupsException(String message) {
1912
                super(message);
×
1913
        }
×
1914

1915
        GroupsException(String message, Throwable cause) {
1916
                super(message, cause);
×
1917
        }
×
1918
}
1919

1920
class NoSuchGroupException extends GroupsException {
1921
        @Serial
1922
        private static final long serialVersionUID = 5193818294198205503L;
1923

1924
        @FormatMethod
1925
        NoSuchGroupException(String msg, Object... args) {
1926
                super(format(msg, args));
×
1927
        }
×
1928
}
1929

1930
class MultipleGroupsException extends GroupsException {
1931
        @Serial
1932
        private static final long serialVersionUID = 6284332340565334236L;
1933

1934
        @FormatMethod
1935
        MultipleGroupsException(String msg, Object... args) {
1936
                super(format(msg, args));
×
1937
        }
×
1938
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc