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

SpiNNakerManchester / JavaSpiNNaker / 6244087494

19 Sep 2023 08:06AM UTC coverage: 36.986% (+0.009%) from 36.977%
6244087494

push

github

web-flow
Merge pull request #1067 from SpiNNakerManchester/fix_machine_location

Fix the location

2 of 2 new or added lines in 1 file covered. (100.0%)

8685 of 23482 relevant lines covered (36.99%)

0.37 hits per line

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

75.16
/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.time.Duration;
40
import java.time.Instant;
41
import java.util.ArrayList;
42
import java.util.Collection;
43
import java.util.Formatter;
44
import java.util.HashMap;
45
import java.util.List;
46
import java.util.Locale;
47
import java.util.Map;
48
import java.util.Optional;
49
import java.util.Set;
50

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

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

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

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

103
        private static final Logger log = getLogger(Spalloc.class);
3✔
104

105
        @Autowired
106
        private PowerController powerController;
107

108
        @Autowired
109
        private Epochs epochs;
110

111
        @Autowired
112
        private QuotaManager quotaManager;
113

114
        @Autowired
115
        private ReportMailSender emailSender;
116

117
        @Autowired
118
        private AllocatorProperties props;
119

120
        @Autowired
121
        private ProxyRememberer rememberer;
122

123
        @Autowired
124
        private AllocatorTask allocator;
125

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

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

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

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

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

151
                private final Query countMachineThings =
3✔
152
                                conn.query(COUNT_MACHINE_THINGS);
3✔
153

154
                private final Query getTags = conn.query(GET_TAGS);
3✔
155

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

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

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

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

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

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

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

220
                final Query countMachineThings = conn.query(COUNT_MACHINE_THINGS);
3✔
221

222
                final Query getTags = conn.query(GET_TAGS);
3✔
223

224
                final Query getJobs = conn.query(GET_MACHINE_JOBS);
3✔
225

226
                final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
3✔
227

228
                final Query getLive = conn.query(GET_LIVE_BOARDS);
3✔
229

230
                final Query getDead = conn.query(GET_DEAD_BOARDS);
3✔
231

232
                final Query getQuota = conn.query(GET_USER_QUOTA);
3✔
233

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

446
                        var scale = props.getPriorityScale();
3✔
447

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

629
        private class BoardLocated {
630
                int boardId;
631

632
                int z;
633

634
                BoardLocated(Row row) {
3✔
635
                        boardId = row.getInt("board_id");
3✔
636
                        z = row.getInt("z");
3✔
637
                }
3✔
638
        }
639

640
        /**
641
         * Resolve a machine name and {@link HasBoardCoords} to a board identifier.
642
         *
643
         * @param conn
644
         *            How to get to the DB.
645
         * @param machineName
646
         *            The name of the machine.
647
         * @param b
648
         *            The request that is the coordinate holder.
649
         * @param requireTriadRoot
650
         *            Whether we require the Z coordinate to be zero.
651
         * @return The board ID.
652
         * @throws IllegalArgumentException
653
         *             If the board doesn't exist or it is a board that is not a
654
         *             root of a triad when a triad root is required.
655
         */
656
        private Integer locateBoard(Connection conn, String machineName,
657
                        HasBoardCoords b, boolean requireTriadRoot) {
658
                try (var findTriad = conn.query(FIND_BOARD_BY_NAME_AND_XYZ);
3✔
659
                                var findPhysical = conn.query(FIND_BOARD_BY_NAME_AND_CFB);
3✔
660
                                var findIP = conn.query(FIND_BOARD_BY_NAME_AND_IP_ADDRESS)) {
3✔
661
                        if (nonNull(b.triad)) {
3✔
662
                                return findTriad.call1(BoardLocated::new,
3✔
663
                                                machineName, b.triad.x, b.triad.y, b.triad.z)
3✔
664
                                                .filter(board -> !requireTriadRoot || board.z == 0)
3✔
665
                                                .map(board -> board.boardId)
3✔
666
                                                .orElseThrow(() -> new IllegalArgumentException(
3✔
667
                                                                NO_BOARD_MSG));
668
                        } else if (nonNull(b.physical)) {
3✔
669
                                return findPhysical.call1(
×
670
                                                BoardLocated::new, machineName, b.physical.c,
×
671
                                                                b.physical.f, b.physical.b)
×
672
                                                .filter(board -> !requireTriadRoot || board.z == 0)
×
673
                                                .map(board -> board.boardId)
×
674
                                                .orElseThrow(() -> new IllegalArgumentException(
×
675
                                                                NO_BOARD_MSG));
676
                        } else {
677
                                return findIP.call1(BoardLocated::new, machineName, b.ip)
3✔
678
                                                .filter(board -> !requireTriadRoot || board.z == 0)
3✔
679
                                                .map(board -> board.boardId)
3✔
680
                                                .orElseThrow(() -> new IllegalArgumentException(
3✔
681
                                                                NO_BOARD_MSG));
682
                        }
683
                }
3✔
684
        }
685

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

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

713
        @Override
714
        public void purgeDownCache() {
715
                synchronized (this) {
×
716
                        downBoardsCache.clear();
×
717
                        downLinksCache.clear();
×
718
                }
×
719
        }
×
720

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

737
        private class Problem {
738
                int boardId;
739

740
                Integer jobId;
741

742
                Problem(Row row) {
3✔
743
                        boardId = row.getInt("board_id");
3✔
744
                        jobId = row.getInt("job_id");
3✔
745
                }
3✔
746
        }
747

748
        @Override
749
        public void reportProblem(String address, HasChipLocation coreLocation,
750
                        String description, Permit permit) {
751
                try (var sql = new BoardReportSQL()) {
3✔
752
                        var desc = mergeDescription(coreLocation, description);
3✔
753
                        var email = sql.transaction(() -> {
3✔
754
                                var machines = getMachines(sql.getConnection(), true).values();
3✔
755
                                for (var m : machines) {
3✔
756
                                        var mail = sql.findBoardNet.call1(
3✔
757
                                                        Problem::new, m.getId(), address)
3✔
758
                                                        .flatMap(prob -> reportProblem(prob, desc, permit,
3✔
759
                                                                        sql));
760
                                        if (mail.isPresent()) {
3✔
761
                                                return mail;
×
762
                                        }
763
                                }
3✔
764
                                return Optional.empty();
3✔
765
                        });
766
                        // Outside the transaction!
767
                        email.ifPresent(emailSender::sendServiceMail);
3✔
768
                } catch (ReportRollbackExn e) {
×
769
                        log.warn("failed to handle problem report", e);
×
770
                }
3✔
771
        }
3✔
772

773
        private Optional<EmailBuilder> reportProblem(Problem problem,
774
                        String description,        Permit permit, BoardReportSQL sql) {
775
                var email = new EmailBuilder(problem.jobId);
3✔
776
                email.header(description, 1, permit.name);
3✔
777
                int userId = getUser(sql.getConnection(), permit.name).orElseThrow(
3✔
778
                                () -> new ReportRollbackExn("no such user: %s", permit.name));
×
779
                sql.insertReport.key(problem.boardId, problem.jobId,
3✔
780
                                description, userId).ifPresent(email::issue);
3✔
781
                return takeBoardsOutOfService(sql, email).map(acted -> {
3✔
782
                        email.footer(acted);
×
783
                        return email;
×
784
                });
785
        }
786

787
        private class Reported {
788
                int boardId;
789

790
                int x;
791

792
                int y;
793

794
                int z;
795

796
                String address;
797

798
                int numReports;
799

800
                Reported(Row row) {
×
801
                        boardId = row.getInt("board_id");
×
802
                        x = row.getInt("x");
×
803
                        y = row.getInt("y");
×
804
                        z = row.getInt("z");
×
805
                        address = row.getString("address");
×
806
                        numReports = row.getInt("numReports");
×
807
                }
×
808

809
        }
810

811
        /**
812
         * Take boards out of service if they've been reported frequently enough.
813
         *
814
         * @param sql
815
         *            How to touch the DB
816
         * @param email
817
         *            The email we are building.
818
         * @return The number of boards taken out of service
819
         */
820
        private Optional<Integer> takeBoardsOutOfService(BoardReportSQL sql,
821
                        EmailBuilder email) {
822
                int acted = 0;
3✔
823
                for (var report : sql.getReported.call(Reported::new,
3✔
824
                                props.getReportActionThreshold())) {
3✔
825
                        if (sql.setFunctioning.call(false, report.boardId) > 0) {
×
826
                                email.serviceActionDone(report);
×
827
                                acted++;
×
828
                        }
829
                }
×
830
                if (acted > 0) {
3✔
831
                        purgeDownCache();
×
832
                }
833
                return acted > 0 ? Optional.of(acted) : Optional.empty();
3✔
834
        }
835

836
        private static DownLink makeDownLinkFromRow(Row row) {
837
                // Non-standard column names to reduce number of queries
838
                var board1 = new BoardCoords(row.getInt("board_1_x"),
×
839
                                row.getInt("board_1_y"), row.getInt("board_1_z"),
×
840
                                row.getInt("board_1_c"), row.getInt("board_1_f"),
×
841
                                row.getInteger("board_1_b"), row.getString("board_1_addr"));
×
842
                var board2 = new BoardCoords(row.getInt("board_2_x"),
×
843
                                row.getInt("board_2_y"), row.getInt("board_2_z"),
×
844
                                row.getInt("board_2_c"), row.getInt("board_2_f"),
×
845
                                row.getInteger("board_2_b"), row.getString("board_2_addr"));
×
846
                return new DownLink(board1, row.getEnum("dir_1", Direction.class),
×
847
                                board2, row.getEnum("dir_2", Direction.class));
×
848
        }
849

850
        private class MachineImpl implements Machine {
851
                private final int id;
852

853
                private final boolean inService;
854

855
                private final String name;
856

857
                private final Set<String> tags;
858

859
                private final int width;
860

861
                private final int height;
862

863
                private boolean lookedUpWraps;
864

865
                private boolean hWrap;
866

867
                private boolean vWrap;
868

869
                @JsonIgnore
870
                private final Epoch epoch;
871

872
                MachineImpl(Connection conn, Row rs) {
3✔
873
                        id = rs.getInt("machine_id");
3✔
874
                        name = rs.getString("machine_name");
3✔
875
                        width = rs.getInt("width");
3✔
876
                        height = rs.getInt("height");
3✔
877
                        inService = rs.getBoolean("in_service");
3✔
878
                        lookedUpWraps = false;
3✔
879
                        try (var getTags = conn.query(GET_TAGS)) {
3✔
880
                                tags = Row.stream(copy(getTags.call(string("tag"), id)))
3✔
881
                                                .toSet();
3✔
882
                        }
883

884
                        this.epoch = epochs.getMachineEpoch(id);
3✔
885
                }
3✔
886

887
                private int getArea() {
888
                        return width * height * TRIAD_DEPTH;
3✔
889
                }
890

891
                @Override
892
                public boolean waitForChange(Duration timeout) {
893
                        if (isNull(epoch)) {
3✔
894
                                log.info("Machine {} epoch is null!", id);
×
895
                                return true;
×
896
                        }
897
                        try {
898
                                log.info("Waiting for change in epoch for {}", id);
3✔
899
                                return epoch.waitForChange(timeout);
3✔
900
                        } catch (InterruptedException interrupted) {
3✔
901
                                log.info("Interrupted waiting for change on {}", id);
3✔
902
                                return false;
3✔
903
                        }
904
                }
905

906
                @Override
907
                public Optional<BoardLocation> getBoardByChip(HasChipLocation chip) {
908
                        try (var conn = getConnection();
3✔
909
                                        var findBoard = conn.query(findBoardByGlobalChip)) {
3✔
910
                                return conn.transaction(false,
3✔
911
                                                () -> findBoard.call1(
3✔
912
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
913
                                                                chip.getX(), chip.getY()));
3✔
914
                        }
915
                }
916

917
                @Override
918
                public Optional<BoardLocation> getBoardByPhysicalCoords(
919
                                PhysicalCoords coords) {
920
                        try (var conn = getConnection();
3✔
921
                                        var findBoard = conn.query(findBoardByPhysicalCoords)) {
3✔
922
                                return conn.transaction(false,
3✔
923
                                                () -> findBoard.call1(
3✔
924
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
925
                                                                coords.c, coords.f, coords.b));
3✔
926
                        }
927
                }
928

929
                @Override
930
                public Optional<BoardLocation> getBoardByLogicalCoords(
931
                                TriadCoords coords) {
932
                        try (var conn = getConnection();
3✔
933
                                        var findBoard = conn.query(findBoardByLogicalCoords)) {
3✔
934
                                return conn.transaction(false,
3✔
935
                                                () -> findBoard.call1(
3✔
936
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
937
                                                                coords.x, coords.y, coords.z));
3✔
938
                        }
939
                }
940

941
                @Override
942
                public Optional<BoardLocation> getBoardByIPAddress(String address) {
943
                        try (var conn = getConnection();
3✔
944
                                        var findBoard = conn.query(findBoardByIPAddress)) {
3✔
945
                                return conn.transaction(false,
3✔
946
                                                () -> findBoard.call1(
3✔
947
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
948
                                                                address));
949
                        }
950
                }
951

952
                @Override
953
                public String getRootBoardBMPAddress() {
954
                        try (var conn = getConnection();
3✔
955
                                        var rootBMPaddr = conn.query(GET_ROOT_BMP_ADDRESS)) {
3✔
956
                                return conn.transaction(false, () -> rootBMPaddr.call1(
3✔
957
                                                string("address"), id).orElse(null));
3✔
958
                        }
959
                }
960

961
                @Override
962
                public List<Integer> getBoardNumbers() {
963
                        try (var conn = getConnection();
3✔
964
                                        var boardNumbers = conn.query(GET_BOARD_NUMBERS)) {
3✔
965
                                return conn.transaction(false, () -> boardNumbers.call(
3✔
966
                                                integer("board_num"), id));
3✔
967
                        }
968
                }
969

970
                @Override
971
                public List<BoardCoords> getDeadBoards() {
972
                        // Assume that the list doesn't change for the duration of this obj
973
                        synchronized (Spalloc.this) {
3✔
974
                                var down = downBoardsCache.get(name);
3✔
975
                                if (nonNull(down)) {
3✔
976
                                        return copy(down);
3✔
977
                                }
978
                        }
3✔
979
                        try (var conn = getConnection();
3✔
980
                                        var boardNumbers = conn.query(GET_DEAD_BOARDS)) {
3✔
981
                                var downBoards = conn.transaction(false,
3✔
982
                                                () -> boardNumbers.call(
3✔
983
                                                                row -> new BoardCoords(row, false), id));
3✔
984
                                synchronized (Spalloc.this) {
3✔
985
                                        downBoardsCache.putIfAbsent(name, downBoards);
3✔
986
                                }
3✔
987
                                return copy(downBoards);
3✔
988
                        }
989
                }
990

991
                @Override
992
                public List<DownLink> getDownLinks() {
993
                        // Assume that the list doesn't change for the duration of this obj
994
                        synchronized (Spalloc.this) {
3✔
995
                                var down = downLinksCache.get(name);
3✔
996
                                if (nonNull(down)) {
3✔
997
                                        return copy(down);
3✔
998
                                }
999
                        }
3✔
1000
                        try (var conn = getConnection();
3✔
1001
                                        var boardNumbers = conn.query(getDeadLinks)) {
3✔
1002
                                var downLinks = conn.transaction(false, () -> boardNumbers
3✔
1003
                                                .call(Spalloc::makeDownLinkFromRow, id));
3✔
1004
                                synchronized (Spalloc.this) {
3✔
1005
                                        downLinksCache.putIfAbsent(name, downLinks);
3✔
1006
                                }
3✔
1007
                                return copy(downLinks);
3✔
1008
                        }
1009
                }
1010

1011
                @Override
1012
                public List<Integer> getAvailableBoards() {
1013
                        try (var conn = getConnection();
3✔
1014
                                        var boardNumbers = conn
3✔
1015
                                                        .query(GET_AVAILABLE_BOARD_NUMBERS)) {
3✔
1016
                                return conn.transaction(false, () -> boardNumbers.call(
3✔
1017
                                                integer("board_num"), id));
3✔
1018
                        }
1019
                }
1020

1021
                @Override
1022
                public int getId() {
1023
                        return id;
3✔
1024
                }
1025

1026
                @Override
1027
                public String getName() {
1028
                        return name;
3✔
1029
                }
1030

1031
                @Override
1032
                public Set<String> getTags() {
1033
                        return tags;
3✔
1034
                }
1035

1036
                @Override
1037
                public int getWidth() {
1038
                        return width;
3✔
1039
                }
1040

1041
                @Override
1042
                public int getHeight() {
1043
                        return height;
3✔
1044
                }
1045

1046
                @Override
1047
                public boolean isInService() {
1048
                        return inService;
3✔
1049
                }
1050

1051
                @Override
1052
                public String getBMPAddress(BMPCoords bmp) {
1053
                        try (var conn = getConnection();
3✔
1054
                                        var bmpAddr = conn.query(GET_BMP_ADDRESS)) {
3✔
1055
                                return conn.transaction(false,
3✔
1056
                                                () -> bmpAddr
1057
                                                                .call1(string("address"), id, bmp.getCabinet(),
3✔
1058
                                                                                bmp.getFrame()).orElse(null));
3✔
1059
                        }
1060
                }
1061

1062
                @Override
1063
                public List<Integer> getBoardNumbers(BMPCoords bmp) {
1064
                        try (var conn = getConnection();
3✔
1065
                                        var boardNumbers = conn.query(GET_BMP_BOARD_NUMBERS)) {
3✔
1066
                                return conn.transaction(false,
3✔
1067
                                                () -> boardNumbers
3✔
1068
                                                                .call(integer("board_num"), id,
3✔
1069
                                                                                bmp.getCabinet(), bmp.getFrame()));
3✔
1070
                        }
1071
                }
1072

1073
                @Override
1074
                public boolean equals(Object other) {
1075
                        // Equality is defined exactly by the database ID
1076
                        return (other instanceof MachineImpl)
×
1077
                                        && (id == ((MachineImpl) other).id);
1078
                }
1079

1080
                @Override
1081
                public int hashCode() {
1082
                        return id;
×
1083
                }
1084

1085
                @Override
1086
                public String toString() {
1087
                        return "Machine(" + name + ")";
×
1088
                }
1089

1090
                private void retrieveWraps() {
1091
                        try (var conn = getConnection();
×
1092
                                        var getWraps = conn.query(GET_MACHINE_WRAPS)) {
×
1093
                                /*
1094
                                 * No locking; not too bothered which thread asks as result will
1095
                                 * be the same either way
1096
                                 */
1097
                                lookedUpWraps =
×
1098
                                                conn.transaction(false, () -> getWraps.call1(rs -> {
×
1099
                                                        hWrap = rs.getBoolean("horizontal_wrap");
×
1100
                                                        vWrap = rs.getBoolean("vertical_wrap");
×
1101
                                                        return true;
×
1102
                                                }, id)).orElse(false);
×
1103
                        }
1104
                }
×
1105

1106
                @Override
1107
                public boolean isHorizonallyWrapped() {
1108
                        if (!lookedUpWraps) {
×
1109
                                retrieveWraps();
×
1110
                        }
1111
                        return hWrap;
×
1112
                }
1113

1114
                @Override
1115
                public boolean isVerticallyWrapped() {
1116
                        if (!lookedUpWraps) {
×
1117
                                retrieveWraps();
×
1118
                        }
1119
                        return vWrap;
×
1120
                }
1121
        }
1122

1123
        private final class JobCollection implements Jobs {
1124
                @JsonIgnore
1125
                private final Epoch epoch;
1126

1127
                private final List<Job> jobs;
1128

1129
                private JobCollection(List<Job> jobs) {
3✔
1130
                        this.jobs = jobs;
3✔
1131
                        if (jobs.isEmpty()) {
3✔
1132
                                epoch = null;
3✔
1133
                        } else {
1134
                                epoch = epochs.getJobsEpoch(
3✔
1135
                                                jobs.stream().map(Job::getId).collect(toList()));
3✔
1136
                        }
1137
                }
3✔
1138

1139
                @Override
1140
                public boolean waitForChange(Duration timeout) {
1141
                        if (isNull(epoch)) {
×
1142
                                return true;
×
1143
                        }
1144
                        try {
1145
                                return epoch.waitForChange(timeout);
×
1146
                        } catch (InterruptedException interrupted) {
×
1147
                                currentThread().interrupt();
×
1148
                                return false;
×
1149
                        }
1150
                }
1151

1152
                /**
1153
                 * Get the set of jobs changed.
1154
                 *
1155
                 * @param timeout
1156
                 *            The timeout to wait for until something happens.
1157
                 * @return The set of changed job identifiers.
1158
                 */
1159
                @Override
1160
                public Collection<Integer> getChanged(Duration timeout) {
1161
                        if (isNull(epoch)) {
3✔
1162
                                return jobs.stream().map(Job::getId).collect(toSet());
3✔
1163
                        }
1164
                        try {
1165
                                return epoch.getChanged(timeout);
×
1166
                        } catch (InterruptedException interrupted) {
×
1167
                                currentThread().interrupt();
×
1168
                                return jobs.stream().map(Job::getId).collect(toSet());
×
1169
                        }
1170
                }
1171

1172
                @Override
1173
                public List<Job> jobs() {
1174
                        return copy(jobs);
×
1175
                }
1176

1177
                @Override
1178
                public List<Integer> ids() {
1179
                        return jobs.stream().map(Job::getId).collect(toList());
3✔
1180
                }
1181
        }
1182

1183
        private final class BoardReportSQL extends AbstractSQL {
3✔
1184
                final Query findBoardByChip = conn.query(findBoardByJobChip);
3✔
1185

1186
                final Query findBoardByTriad = conn.query(findBoardByLogicalCoords);
3✔
1187

1188
                final Query findBoardPhys = conn.query(findBoardByPhysicalCoords);
3✔
1189

1190
                final Query findBoardNet = conn.query(findBoardByIPAddress);
3✔
1191

1192
                final Update insertReport = conn.update(INSERT_BOARD_REPORT);
3✔
1193

1194
                final Query getReported = conn.query(getReportedBoards);
3✔
1195

1196
                final Update setFunctioning = conn.update(SET_FUNCTIONING_FIELD);
3✔
1197

1198
                final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
3✔
1199

1200
                @Override
1201
                public void close() {
1202
                        findBoardByChip.close();
3✔
1203
                        findBoardByTriad.close();
3✔
1204
                        findBoardPhys.close();
3✔
1205
                        findBoardNet.close();
3✔
1206
                        insertReport.close();
3✔
1207
                        getReported.close();
3✔
1208
                        setFunctioning.close();
3✔
1209
                        getNamedMachine.close();
3✔
1210
                        super.close();
3✔
1211
                }
3✔
1212
        }
1213

1214
        /** Used to assemble an issue-report email for sending. */
1215
        private static final class EmailBuilder {
1216
                /**
1217
                 * More efficient than several String.format() calls, and much clearer
1218
                 * than a mess of direct {@link StringBuilder} calls!
1219
                 */
1220
                private final Formatter b = new Formatter(Locale.UK);
3✔
1221

1222
                private final int id;
1223

1224
                /**
1225
                 * @param id
1226
                 *            The job ID
1227
                 */
1228
                EmailBuilder(int id) {
3✔
1229
                        this.id = id;
3✔
1230
                }
3✔
1231

1232
                void header(String issue, int numBoards, String who) {
1233
                        b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
3✔
1234
                                        numBoards, who);
3✔
1235
                }
3✔
1236

1237
                void chip(ReportedBoard board) {
1238
                        b.format("\tBoard for job (%d) chip %s\n", //
×
1239
                                        id, board.chip);
×
1240
                }
×
1241

1242
                void triad(ReportedBoard board) {
1243
                        b.format("\tBoard for job (%d) board (X:%d,Y:%d,Z:%d)\n", //
×
1244
                                        id, board.x, board.y, board.z);
×
1245
                }
×
1246

1247
                void phys(ReportedBoard board) {
1248
                        b.format(
×
1249
                                        "\tBoard for job (%d) board "
1250
                                                        + "[Cabinet:%d,Frame:%d,Board:%d]\n", //
1251
                                        id, board.cabinet, board.frame, board.board);
×
1252
                }
×
1253

1254
                void ip(ReportedBoard board) {
1255
                        b.format("\tBoard for job (%d) board (IP: %s)\n", //
3✔
1256
                                        id, board.address);
3✔
1257
                }
3✔
1258

1259
                void issue(int issueId) {
1260
                        b.format("\t\tAction: noted as issue #%d\n", //
3✔
1261
                                        issueId);
3✔
1262
                }
3✔
1263

1264
                void footer(int numActions) {
1265
                        b.format("\nSummary: %d boards taken out of service.\n",
×
1266
                                        numActions);
×
1267
                }
×
1268

1269
                void serviceActionDone(Reported report) {
1270
                        b.format(
×
1271
                                        "\tAction: board (X:%d,Y:%d,Z:%d) (IP: %s) "
1272
                                                        + "taken out of service once not in use "
1273
                                                        + "(%d problems reported)\n",
1274
                                        report.x, report.y, report.z,
×
1275
                                        report.address, report.numReports);
×
1276
                }
×
1277

1278
                /** @return The assembled message body. */
1279
                @Override
1280
                public String toString() {
1281
                        return b.toString();
×
1282
                }
1283
        }
1284

1285
        private final class JobImpl implements Job {
1286
                @JsonIgnore
1287
                private Epoch epoch;
1288

1289
                private final int id;
1290

1291
                private final int machineId;
1292

1293
                private Integer width;
1294

1295
                private Integer height;
1296

1297
                private Integer depth;
1298

1299
                private JobState state;
1300

1301
                /** If not {@code null}, the ID of the root board of the job. */
1302
                private Integer root;
1303

1304
                private ChipLocation chipRoot;
1305

1306
                private String owner;
1307

1308
                private String keepaliveHost;
1309

1310
                private Instant startTime;
1311

1312
                private Instant keepaliveTime;
1313

1314
                private Instant finishTime;
1315

1316
                private String deathReason;
1317

1318
                private byte[] request;
1319

1320
                private boolean partial;
1321

1322
                private MachineImpl cachedMachine;
1323

1324
                JobImpl(int id, int machineId) {
3✔
1325
                        this.epoch = epochs.getJobsEpoch(id);
3✔
1326
                        this.id = id;
3✔
1327
                        this.machineId = machineId;
3✔
1328
                        partial = true;
3✔
1329
                }
3✔
1330

1331
                JobImpl(int jobId, int machineId, JobState jobState,
1332
                                Instant keepalive) {
1333
                        this(jobId, machineId);
3✔
1334
                        state = jobState;
3✔
1335
                        keepaliveTime = keepalive;
3✔
1336
                }
3✔
1337

1338
                JobImpl(Connection conn, Row row) {
3✔
1339
                        this.id = row.getInt("job_id");
3✔
1340
                        this.machineId = row.getInt("machine_id");
3✔
1341
                        width = row.getInteger("width");
3✔
1342
                        height = row.getInteger("height");
3✔
1343
                        depth = row.getInteger("depth");
3✔
1344
                        root = row.getInteger("root_id");
3✔
1345
                        owner = row.getString("owner");
3✔
1346
                        if (nonNull(root)) {
3✔
1347
                                try (var boardRoot = conn.query(GET_ROOT_OF_BOARD)) {
3✔
1348
                                        chipRoot = boardRoot.call1(chip("root_x", "root_y"), root)
3✔
1349
                                                        .orElse(null);
3✔
1350
                                }
1351
                        }
1352
                        state = row.getEnum("job_state", JobState.class);
3✔
1353
                        keepaliveHost = row.getString("keepalive_host");
3✔
1354
                        keepaliveTime = row.getInstant("keepalive_timestamp");
3✔
1355
                        startTime = row.getInstant("create_timestamp");
3✔
1356
                        finishTime = row.getInstant("death_timestamp");
3✔
1357
                        deathReason = row.getString("death_reason");
3✔
1358
                        request = row.getBytes("original_request");
3✔
1359
                        partial = false;
3✔
1360

1361
                        this.epoch = epochs.getJobsEpoch(id);
3✔
1362
                }
3✔
1363

1364
                /**
1365
                 * Get the machine that this job is running on. May used a cached value.
1366
                 * A transaction is required, but may be a read-only transaction.
1367
                 *
1368
                 * @param conn
1369
                 *            The connection to the DB
1370
                 * @return The overall machine handle.
1371
                 */
1372
                private synchronized MachineImpl getJobMachine(Connection conn) {
1373
                        if (cachedMachine == null || !cachedMachine.epoch.isValid()) {
3✔
1374
                                cachedMachine = Spalloc.this.getMachine(machineId, true, conn)
3✔
1375
                                                .orElseThrow();
3✔
1376
                        }
1377
                        return cachedMachine;
3✔
1378
                }
1379

1380
                @Override
1381
                public void access(String keepaliveAddress) {
1382
                        if (partial) {
3✔
1383
                                throw new PartialJobException();
×
1384
                        }
1385
                        try (var conn = getConnection();
3✔
1386
                                        var keepAlive = conn.update(UPDATE_KEEPALIVE)) {
3✔
1387
                                conn.transaction(() -> keepAlive.call(keepaliveAddress, id));
3✔
1388
                        }
1389
                }
3✔
1390

1391
                @Override
1392
                public void destroy(String reason) {
1393
                        if (partial) {
3✔
1394
                                throw new PartialJobException();
×
1395
                        }
1396
                        powerController.destroyJob(id, reason);
3✔
1397
                        rememberer.killProxies(id);
3✔
1398
                }
3✔
1399

1400
                @Override
1401
                public boolean waitForChange(Duration timeout) {
1402
                        if (isNull(epoch)) {
3✔
1403
                                return true;
×
1404
                        }
1405
                        try {
1406
                                return epoch.waitForChange(timeout);
×
1407
                        } catch (InterruptedException interrupted) {
3✔
1408
                                currentThread().interrupt();
3✔
1409
                                return false;
3✔
1410
                        }
1411
                }
1412

1413
                @Override
1414
                public int getId() {
1415
                        return id;
3✔
1416
                }
1417

1418
                @Override
1419
                public JobState getState() {
1420
                        return state;
3✔
1421
                }
1422

1423
                @Override
1424
                public Instant getStartTime() {
1425
                        return startTime;
3✔
1426
                }
1427

1428
                @Override
1429
                public Optional<Instant> getFinishTime() {
1430
                        return Optional.ofNullable(finishTime);
3✔
1431
                }
1432

1433
                @Override
1434
                public Optional<String> getReason() {
1435
                        return Optional.ofNullable(deathReason);
3✔
1436
                }
1437

1438
                @Override
1439
                public Optional<String> getKeepaliveHost() {
1440
                        if (partial) {
3✔
1441
                                return Optional.empty();
×
1442
                        }
1443
                        return Optional.ofNullable(keepaliveHost);
3✔
1444
                }
1445

1446
                @Override
1447
                public Instant getKeepaliveTimestamp() {
1448
                        return keepaliveTime;
3✔
1449
                }
1450

1451
                @Override
1452
                public Optional<byte[]> getOriginalRequest() {
1453
                        if (partial) {
3✔
1454
                                return Optional.empty();
×
1455
                        }
1456
                        return Optional.ofNullable(request);
3✔
1457
                }
1458

1459
                @Override
1460
                public Optional<SubMachine> getMachine() {
1461
                        if (isNull(root)) {
3✔
1462
                                return Optional.empty();
3✔
1463
                        }
1464
                        return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
3✔
1465
                }
1466

1467
                @Override
1468
                public Optional<BoardLocation> whereIs(int x, int y) {
1469
                        if (isNull(root)) {
3✔
1470
                                return Optional.empty();
×
1471
                        }
1472
                        try (var conn = getConnection();
3✔
1473
                                        var findBoard = conn.query(findBoardByJobChip)) {
3✔
1474
                                return conn.transaction(false, () -> findBoard
3✔
1475
                                                .call1(row -> new BoardLocationImpl(row,
3✔
1476
                                                                getJobMachine(conn)), id, root, x, y));
3✔
1477
                        }
1478
                }
1479

1480
                // -------------------------------------------------------------
1481
                // Bad board report handling
1482

1483
                @Override
1484
                public String reportIssue(IssueReportRequest report, Permit permit) {
1485
                        try (var q = new BoardReportSQL()) {
3✔
1486
                                var email = new EmailBuilder(id);
3✔
1487
                                var result = q.transaction(
3✔
1488
                                                () -> reportIssue(report, permit, email, q));
3✔
1489
                                emailSender.sendServiceMail(email);
3✔
1490
                                for (var m : report.boards.stream()
3✔
1491
                                                .map(b -> q.getNamedMachine.call1(
3✔
1492
                                                                r -> r.getInt("machine_id"), b.machine, true))
3✔
1493
                                                .collect(toSet())) {
3✔
1494
                                        if (m.isPresent()) {
3✔
1495
                                                epochs.machineChanged(m.get());
×
1496
                                        }
1497
                                }
3✔
1498

1499
                                return result;
3✔
1500
                        } catch (ReportRollbackExn e) {
×
1501
                                return e.getMessage();
×
1502
                        }
1503
                }
1504

1505
                /**
1506
                 * Report an issue with some boards and assemble the email to send. This
1507
                 * may result in boards being taken out of service (i.e., no longer
1508
                 * being available to be allocated; their current allocation will
1509
                 * continue).
1510
                 * <p>
1511
                 * <strong>NB:</strong> The sending of the email sending is
1512
                 * <em>outside</em> the transaction that this code is executed in.
1513
                 *
1514
                 * @param report
1515
                 *            The report from the user.
1516
                 * @param permit
1517
                 *            Who the user is.
1518
                 * @param email
1519
                 *            The email we're assembling.
1520
                 * @param q
1521
                 *            SQL access queries.
1522
                 * @return Summary of action taken message, to go to user.
1523
                 * @throws ReportRollbackExn
1524
                 *             If the report is bad somehow.
1525
                 */
1526
                private String reportIssue(IssueReportRequest report, Permit permit,
1527
                                EmailBuilder email, BoardReportSQL q) throws ReportRollbackExn {
1528
                        email.header(report.issue, report.boards.size(), permit.name);
3✔
1529
                        int userId = getUser(q.getConnection(), permit.name)
3✔
1530
                                        .orElseThrow(() -> new ReportRollbackExn(
3✔
1531
                                                        "no such user: %s", permit.name));
1532
                        for (var board : report.boards) {
3✔
1533
                                addIssueReport(q, getJobBoardForReport(q, board, email),
3✔
1534
                                                report.issue, userId, email);
1535
                        }
3✔
1536
                        return takeBoardsOutOfService(q, email).map(acted -> {
3✔
1537
                                email.footer(acted);
×
1538
                                return format("%d boards taken out of service", acted);
×
1539
                        }).orElse("report noted");
3✔
1540
                }
1541

1542
                /**
1543
                 * Convert a board locator (for an issue report) into a board ID.
1544
                 *
1545
                 * @param q
1546
                 *            How to touch the DB
1547
                 * @param board
1548
                 *            What board are we talking about
1549
                 * @param email
1550
                 *            The email we are building.
1551
                 * @return The board ID
1552
                 * @throws ReportRollbackExn
1553
                 *             If the board can't be converted to an ID
1554
                 */
1555
                private int getJobBoardForReport(BoardReportSQL q, ReportedBoard board,
1556
                                EmailBuilder email) throws ReportRollbackExn {
1557
                        Problem r;
1558
                        if (nonNull(board.chip)) {
3✔
1559
                                r = q.findBoardByChip
×
1560
                                                .call1(Problem::new, id, root, board.chip.getX(),
×
1561
                                                                board.chip.getY())
×
1562
                                                .orElseThrow(() -> new ReportRollbackExn(board.chip));
×
1563
                                email.chip(board);
×
1564
                        } else if (nonNull(board.x)) {
3✔
1565
                                r = q.findBoardByTriad
×
1566
                                                .call1(Problem::new, machineId, board.x, board.y,
×
1567
                                                                board.z)
1568
                                                .orElseThrow(() -> new ReportRollbackExn(
×
1569
                                                                "triad (%s,%s,%s) not in machine", board.x,
1570
                                                                board.y, board.z));
1571
                                if (isNull(r.jobId) || id != r.jobId) {
×
1572
                                        throw new ReportRollbackExn(
×
1573
                                                        "triad (%s,%s,%s) not allocated to job %d", board.x,
1574
                                                        board.y, board.z, id);
×
1575
                                }
1576
                                email.triad(board);
×
1577
                        } else if (nonNull(board.cabinet)) {
3✔
1578
                                r = q.findBoardPhys
×
1579
                                                .call1(Problem::new, machineId, board.cabinet,
×
1580
                                                                board.frame, board.board)
1581
                                                .orElseThrow(() -> new ReportRollbackExn(
×
1582
                                                                "physical board [%s,%s,%s] not in machine",
1583
                                                                board.cabinet, board.frame, board.board));
1584
                                if (isNull(r.jobId) || id != r.jobId) {
×
1585
                                        throw new ReportRollbackExn(
×
1586
                                                        "physical board [%s,%s,%s] not allocated to job %d",
1587
                                                        board.cabinet, board.frame, board.board, id);
×
1588
                                }
1589
                                email.phys(board);
×
1590
                        } else if (nonNull(board.address)) {
3✔
1591
                                r = q.findBoardNet.call1(Problem::new, machineId, board.address)
3✔
1592
                                                .orElseThrow(() -> new ReportRollbackExn(
3✔
1593
                                                                "board at %s not in machine", board.address));
1594
                                if (isNull(r.jobId) || id != r.jobId) {
3✔
1595
                                        throw new ReportRollbackExn(
×
1596
                                                        "board at %s not allocated to job %d",
1597
                                                        board.address, id);
×
1598
                                }
1599
                                email.ip(board);
3✔
1600
                        } else {
1601
                                throw new UnsupportedOperationException();
×
1602
                        }
1603
                        return r.boardId;
3✔
1604
                }
1605

1606
                /**
1607
                 * Record a reported issue with a board.
1608
                 *
1609
                 * @param u
1610
                 *            How to touch the DB
1611
                 * @param boardId
1612
                 *            What board has the issue?
1613
                 * @param issue
1614
                 *            What is the issue?
1615
                 * @param userId
1616
                 *            Who is doing the report?
1617
                 * @param email
1618
                 *            The email we are building.
1619
                 */
1620
                private void addIssueReport(BoardReportSQL u, int boardId, String issue,
1621
                                int userId, EmailBuilder email) {
1622
                        u.insertReport.key(boardId, id, issue, userId)
3✔
1623
                                        .ifPresent(email::issue);
3✔
1624
                }
3✔
1625

1626
                // -------------------------------------------------------------
1627

1628
                @Override
1629
                public Optional<ChipLocation> getRootChip() {
1630
                        return Optional.ofNullable(chipRoot);
3✔
1631
                }
1632

1633
                @Override
1634
                public Optional<String> getOwner() {
1635
                        if (partial) {
3✔
1636
                                return Optional.empty();
×
1637
                        }
1638
                        return Optional.ofNullable(owner);
3✔
1639
                }
1640

1641
                @Override
1642
                public Optional<Integer> getWidth() {
1643
                        return Optional.ofNullable(width);
3✔
1644
                }
1645

1646
                @Override
1647
                public Optional<Integer> getHeight() {
1648
                        return Optional.ofNullable(height);
3✔
1649
                }
1650

1651
                @Override
1652
                public Optional<Integer> getDepth() {
1653
                        return Optional.ofNullable(depth);
3✔
1654
                }
1655

1656
                @Override
1657
                public void rememberProxy(ProxyCore proxy) {
1658
                        rememberer.rememberProxyForJob(id, proxy);
×
1659
                }
×
1660

1661
                @Override
1662
                public void forgetProxy(ProxyCore proxy) {
1663
                        rememberer.removeProxyForJob(id, proxy);
×
1664
                }
×
1665

1666
                @Override
1667
                public boolean equals(Object other) {
1668
                        // Equality is defined exactly by the database ID
1669
                        return (other instanceof JobImpl) && (id == ((JobImpl) other).id);
×
1670
                }
1671

1672
                @Override
1673
                public int hashCode() {
1674
                        return id;
×
1675
                }
1676

1677
                @Override
1678
                public String toString() {
1679
                        return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1680
                                        width, height, depth, startTime, finishTime);
1681
                }
1682

1683
                private final class SubMachineImpl implements SubMachine {
1684
                        /** The machine that this sub-machine is part of. */
1685
                        private final Machine machine;
1686

1687
                        /** The root X coordinate of this sub-machine. */
1688
                        private int rootX;
1689

1690
                        /** The root Y coordinate of this sub-machine. */
1691
                        private int rootY;
1692

1693
                        /** The root Z coordinate of this sub-machine. */
1694
                        private int rootZ;
1695

1696
                        /** The connection details of this sub-machine. */
1697
                        private List<ConnectionInfo> connections;
1698

1699
                        /** The board locations of this sub-machine. */
1700
                        private List<BoardCoordinates> boards;
1701

1702
                        private List<Integer> boardIds;
1703

1704
                        private SubMachineImpl(Connection conn) {
3✔
1705
                                machine = getJobMachine(conn);
3✔
1706
                                try (var getRootXY = conn.query(GET_ROOT_COORDS);
3✔
1707
                                                var getBoardInfo = conn.query(GET_BOARD_CONNECT_INFO)) {
3✔
1708
                                        getRootXY.call1(row -> {
3✔
1709
                                                rootX = row.getInt("x");
3✔
1710
                                                rootY = row.getInt("y");
3✔
1711
                                                rootZ = row.getInt("z");
3✔
1712
                                                // We have to return something,
1713
                                                // but it doesn't matter what
1714
                                                return true;
3✔
1715
                                        }, root);
1716
                                        int capacityEstimate = width * height;
3✔
1717
                                        connections = new ArrayList<>(capacityEstimate);
3✔
1718
                                        boards = new ArrayList<>(capacityEstimate);
3✔
1719
                                        boardIds = new ArrayList<>(capacityEstimate);
3✔
1720
                                        getBoardInfo.call(row -> {
3✔
1721
                                                boardIds.add(row.getInt("board_id"));
3✔
1722
                                                boards.add(new BoardCoordinates(row.getInt("x"),
3✔
1723
                                                                row.getInt("y"), row.getInt("z")));
3✔
1724
                                                connections.add(new ConnectionInfo(
3✔
1725
                                                                relativeChipLocation(row.getInt("root_x"),
3✔
1726
                                                                                row.getInt("root_y")),
3✔
1727
                                                                row.getString("address")));
3✔
1728
                                                // We have to return something,
1729
                                                // but it doesn't matter what
1730
                                                return true;
3✔
1731
                                        }, id);
3✔
1732
                                }
1733
                        }
3✔
1734

1735
                        private ChipLocation relativeChipLocation(int x, int y) {
1736
                                x -= chipRoot.getX();
3✔
1737
                                y -= chipRoot.getY();
3✔
1738
                                // Allow for wrapping
1739
                                if (x < 0) {
3✔
1740
                                        x += machine.getWidth() * TRIAD_CHIP_SIZE;
×
1741
                                }
1742
                                if (y < 0) {
3✔
1743
                                        y += machine.getHeight() * TRIAD_CHIP_SIZE;
×
1744
                                }
1745
                                return new ChipLocation(x, y);
3✔
1746
                        }
1747

1748
                        @Override
1749
                        public Machine getMachine() {
1750
                                return machine;
3✔
1751
                        }
1752

1753
                        @Override
1754
                        public int getRootX() {
1755
                                return rootX;
3✔
1756
                        }
1757

1758
                        @Override
1759
                        public int getRootY() {
1760
                                return rootY;
3✔
1761
                        }
1762

1763
                        @Override
1764
                        public int getRootZ() {
1765
                                return rootZ;
3✔
1766
                        }
1767

1768
                        @Override
1769
                        public int getWidth() {
1770
                                return width;
3✔
1771
                        }
1772

1773
                        @Override
1774
                        public int getHeight() {
1775
                                return height;
3✔
1776
                        }
1777

1778
                        @Override
1779
                        public int getDepth() {
1780
                                return depth;
3✔
1781
                        }
1782

1783
                        @Override
1784
                        public List<ConnectionInfo> getConnections() {
1785
                                return connections;
3✔
1786
                        }
1787

1788
                        @Override
1789
                        public List<BoardCoordinates> getBoards() {
1790
                                return boards;
3✔
1791
                        }
1792

1793
                        @Override
1794
                        public PowerState getPower() {
1795
                                try (var conn = getConnection();
3✔
1796
                                                var power = conn.query(GET_SUM_BOARDS_POWERED)) {
3✔
1797
                                        return conn.transaction(false,
3✔
1798
                                                        () -> power.call1(integer("total_on"), id)
3✔
1799
                                                                        .map(totalOn -> totalOn < boardIds.size()
3✔
1800
                                                                                        ? OFF
3✔
1801
                                                                                        : ON)
×
1802
                                                                        .orElse(null));
3✔
1803
                                }
1804
                        }
1805

1806
                        @Override
1807
                        public void setPower(PowerState ps) {
1808
                                if (partial) {
3✔
1809
                                        throw new PartialJobException();
×
1810
                                }
1811
                                powerController.setPower(id, ps, READY);
3✔
1812
                        }
3✔
1813
                }
1814
        }
1815

1816
        /**
1817
         * Board location implementation. Does not retain database connections after
1818
         * creation.
1819
         *
1820
         * @author Donal Fellows
1821
         */
1822
        private final class BoardLocationImpl implements BoardLocation {
1823
                private JobImpl job;
1824

1825
                private final String machineName;
1826

1827
                private final int machineWidth;
1828

1829
                private final int machineHeight;
1830

1831
                private final ChipLocation chip;
1832

1833
                private final ChipLocation boardChip;
1834

1835
                private final BoardCoordinates logical;
1836

1837
                private final BoardPhysicalCoordinates physical;
1838

1839
                // Transaction is open
1840
                private BoardLocationImpl(Row row, Machine machine) {
3✔
1841
                        machineName = row.getString("machine_name");
3✔
1842
                        logical = new BoardCoordinates(row.getInt("x"), row.getInt("y"),
3✔
1843
                                        row.getInt("z"));
3✔
1844
                        physical = new BoardPhysicalCoordinates(row.getInt("cabinet"),
3✔
1845
                                        row.getInt("frame"), row.getInteger("board_num"));
3✔
1846
                        chip = row.getChip("chip_x", "chip_y");
3✔
1847
                        machineWidth = machine.getWidth();
3✔
1848
                        machineHeight = machine.getHeight();
3✔
1849
                        var boardX = row.getInteger("board_chip_x");
3✔
1850
                        if (nonNull(boardX)) {
3✔
1851
                                boardChip = row.getChip("board_chip_x", "board_chip_y");
3✔
1852
                        } else {
1853
                                boardChip = chip;
×
1854
                        }
1855

1856
                        var jobId = row.getInteger("job_id");
3✔
1857
                        if (nonNull(jobId)) {
3✔
1858
                                job = new JobImpl(jobId, machine.getId());
3✔
1859
                                job.chipRoot = row.getChip("job_root_chip_x",
3✔
1860
                                                "job_root_chip_y");
1861
                        }
1862
                }
3✔
1863

1864
                @Override
1865
                public ChipLocation getBoardChip() {
1866
                        return boardChip;
3✔
1867
                }
1868

1869
                @Override
1870
                public ChipLocation getChipRelativeTo(ChipLocation rootChip) {
1871
                        int x = chip.getX() - rootChip.getX();
3✔
1872
                        if (x < 0) {
3✔
1873
                                x += machineWidth * TRIAD_CHIP_SIZE;
×
1874
                        }
1875
                        int y = chip.getY() - rootChip.getY();
3✔
1876
                        if (y < 0) {
3✔
1877
                                y += machineHeight * TRIAD_CHIP_SIZE;
×
1878
                        }
1879
                        return new ChipLocation(x, y);
3✔
1880
                }
1881

1882
                @Override
1883
                public String getMachine() {
1884
                        return machineName;
3✔
1885
                }
1886

1887
                @Override
1888
                public BoardCoordinates getLogical() {
1889
                        return logical;
3✔
1890
                }
1891

1892
                @Override
1893
                public BoardPhysicalCoordinates getPhysical() {
1894
                        return physical;
3✔
1895
                }
1896

1897
                @Override
1898
                public ChipLocation getChip() {
1899
                        return chip;
3✔
1900
                }
1901

1902
                @Override
1903
                public Job getJob() {
1904
                        return job;
3✔
1905
                }
1906
        }
1907

1908
        static class PartialJobException extends IllegalStateException {
1909
                private static final long serialVersionUID = 2997856394666135483L;
1910

1911
                PartialJobException() {
1912
                        super("partial job only");
×
1913
                }
×
1914
        }
1915
}
1916

1917
class ReportRollbackExn extends RuntimeException {
1918
        private static final long serialVersionUID = 1L;
1919

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

1925
        ReportRollbackExn(HasChipLocation chip) {
1926
                this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
1927
                                chip.getY());
×
1928
        }
×
1929
}
1930

1931
abstract class GroupsException extends RuntimeException {
1932
        private static final long serialVersionUID = 6607077117924279611L;
1933

1934
        GroupsException(String message) {
1935
                super(message);
×
1936
        }
×
1937

1938
        GroupsException(String message, Throwable cause) {
1939
                super(message, cause);
×
1940
        }
×
1941
}
1942

1943
class NoSuchGroupException extends GroupsException {
1944
        private static final long serialVersionUID = 5193818294198205503L;
1945

1946
        @FormatMethod
1947
        NoSuchGroupException(String msg, Object... args) {
1948
                super(format(msg, args));
×
1949
        }
×
1950
}
1951

1952
class MultipleGroupsException extends GroupsException {
1953
        private static final long serialVersionUID = 6284332340565334236L;
1954

1955
        @FormatMethod
1956
        MultipleGroupsException(String msg, Object... args) {
1957
                super(format(msg, args));
×
1958
        }
×
1959
}
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