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

SpiNNakerManchester / JavaSpiNNaker / 7017

01 Sep 2025 09:45AM UTC coverage: 36.241% (-0.04%) from 36.277%
7017

push

github

web-flow
Merge pull request #1296 from SpiNNakerManchester/default_user_quota

Default user quota

1909 of 5896 branches covered (32.38%)

Branch coverage included in aggregate %.

7 of 18 new or added lines in 3 files covered. (38.89%)

12 existing lines in 4 files now uncovered.

8959 of 24092 relevant lines covered (37.19%)

0.74 hits per line

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

65.72
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/allocator/Spalloc.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package uk.ac.manchester.spinnaker.alloc.allocator;
17

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

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

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

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

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

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

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

119
        @Autowired
120
        private PowerController powerController;
121

122
        @Autowired
123
        private Epochs epochs;
124

125
        @Autowired
126
        private QuotaManager quotaManager;
127

128
        @Autowired
129
        private ReportMailSender emailSender;
130

131
        @Autowired
132
        private AllocatorProperties props;
133

134
        @Autowired
135
        private AuthProperties authProps;
136

137
        @Autowired
138
        private JobObjectRememberer rememberer;
139

140
        @Autowired
141
        private AllocatorTask allocator;
142

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

147
        @GuardedBy("this")
2✔
148
        private transient Map<String, List<DownLink>> downLinksCache =
149
                        new HashMap<>();
150

151
        private boolean emergencyStop = false;
2✔
152

153
        @Override
154
        public Map<String, Machine> getMachines(boolean allowOutOfService) {
155
                return executeRead(c -> getMachines(c, allowOutOfService));
2✔
156
        }
157

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

167
        private final class ListMachinesSQL extends AbstractSQL {
2✔
168
                private final Query listMachines = conn.query(GET_ALL_MACHINES);
2✔
169

170
                private final Query countMachineThings =
2✔
171
                                conn.query(COUNT_MACHINE_THINGS);
2✔
172

173
                private final Query getTags = conn.query(GET_TAGS);
2✔
174

175
                @Override
176
                public void close() {
177
                        listMachines.close();
2✔
178
                        countMachineThings.close();
2✔
179
                        getTags.close();
2✔
180
                        super.close();
2✔
181
                }
2✔
182

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

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

213
        @Override
214
        public Optional<Machine> getMachine(String name,
215
                        boolean allowOutOfService) {
216
                return executeRead(
2✔
217
                                conn -> getMachine(name, allowOutOfService, conn).map(m -> m));
2✔
218
        }
219

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

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

236
        private final class DescribeMachineSQL extends AbstractSQL {
2✔
237
                private final Query namedMachine = conn.query(GET_NAMED_MACHINE);
2✔
238

239
                private final Query countMachineThings = conn.query(
2✔
240
                                COUNT_MACHINE_THINGS);
241

242
                private final Query getTags = conn.query(GET_TAGS);
2✔
243

244
                private final Query getJobs = conn.query(GET_MACHINE_JOBS);
2✔
245

246
                private final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
2✔
247

248
                private final Query getLive = conn.query(GET_LIVE_BOARDS);
2✔
249

250
                private final Query getDead = conn.query(GET_DEAD_BOARDS);
2✔
251

252
                private final Query getQuota = conn.query(GET_USER_QUOTA);
2✔
253

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

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

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

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

309
                var ji = new JobInfo();
2✔
310
                ji.setId(jobId);
2✔
311
                ji.setOwner(owner);
2✔
312
                ji.setBoards(
2✔
313
                                getCoords.call(r -> new BoardCoords(r, !mayUnveil), jobId));
2!
314
                return ji;
2✔
315
        }
316

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

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

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

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

384
        @Override
385
        @PostFilter(MAY_SEE_JOB_DETAILS)
386
        public Optional<Job> getJob(Permit permit, int id) {
387
                return executeRead(conn -> getJob(id, conn).map(j -> (Job) j));
2✔
388
        }
389

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

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

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

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

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

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

472
                        var scale = props.getPriorityScale();
2✔
473

474
                        if (machine.getArea() < descriptor.getArea()) {
2!
475
                                throw new IllegalArgumentException(
×
476
                                                "request cannot fit on machine");
477
                        }
478

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

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

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

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

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

540
                        allocator.scheduleAllocateNow();
2✔
541
                        return getJob(jobId, conn).map(ji -> (Job) ji).orElseThrow(
2✔
542
                                        () -> new RuntimeException("Error creating job!"));
×
543
                });
544
        }
545

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

555
                        // OIDC users can use a private group
556
                        log.debug("User {} is {}internal", owner, isInternal ? "" : "not ");
2!
557
                        if (!isInternal) {
2!
NEW
558
                                var oidUser = owner.substring(
×
NEW
559
                                                authProps.getOpenid().getUsernamePrefix().length());
×
NEW
560
                                return createJobInCollabSession(
×
561
                                                owner, PRIVATE_COLLAB_PREFIX + oidUser, descriptor,
562
                                                machineName, tags, keepaliveInterval, originalRequest);
563
                        }
564
                        return createJobInGroup(
2✔
565
                                owner, getOnlyGroup(conn, owner), descriptor, machineName,
2✔
566
                                tags, keepaliveInterval, originalRequest);
567
                });
568
        }
569

570
        @Override
571
        public Job createJobInCollabSession(String owner,
572
                        String nmpiCollab, CreateDescriptor descriptor,
573
                        String machineName, List<String> tags, Duration keepaliveInterval,
574
                        byte[] originalRequest) {
575
                var session = quotaManager.createSession(nmpiCollab, owner);
×
576
                var quotaUnits = session.getResourceUsage().getUnits();
×
577

578
                // Use the Collab name as the group, as it should exist
579
                var job = execute(conn -> createJobInGroup(
×
580
                                owner, nmpiCollab, descriptor, machineName,
581
                                tags, keepaliveInterval, originalRequest));
582

583
                quotaManager.associateNMPISession(job.getId(), session.getId(),
×
584
                                quotaUnits);
585

586
                // Return the job created
587
                return job;
×
588
        }
589

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

601
                var job = execute(conn -> createJobInGroup(
×
602
                                owner, quotaDetails.collabId, descriptor, machineName,
603
                                tags, keepaliveInterval, originalRequest));
604

605
                quotaManager.associateNMPIJob(job.getId(), nmpiJobId,
×
606
                                quotaDetails.quotaUnits);
607

608
                // Return the job created
609
                return job;
×
610
        }
611

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

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

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

660
        private class BoardLocated {
661
                private int boardId;
662

663
                private int z;
664

665
                BoardLocated(Row row) {
2✔
666
                        boardId = row.getInt("board_id");
2✔
667
                        z = row.getInt("z");
2✔
668
                }
2✔
669
        }
670

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

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

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

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

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

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

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

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

793
        @Override
794
        public void purgeDownCache() {
795
                synchronized (this) {
×
796
                        downBoardsCache.clear();
×
797
                        downLinksCache.clear();
×
798
                }
×
799
        }
×
800

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

817
        private class Problem {
818
                private int boardId;
819

820
                private Integer jobId;
821

822
                Problem(Row row) {
2✔
823
                        boardId = row.getInt("board_id");
2✔
824
                        jobId = row.getInt("job_id");
2✔
825
                }
2✔
826
        }
827

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

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

867
        private class Reported {
868
                private int boardId;
869

870
                private int x;
871

872
                private int y;
873

874
                private int z;
875

876
                private String address;
877

878
                private int numReports;
879

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

889
        }
890

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

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

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

939
        private class MachineImpl implements Machine {
940
                private final int id;
941

942
                private final boolean inService;
943

944
                private final String name;
945

946
                private final Set<String> tags;
947

948
                private final int width;
949

950
                private final int height;
951

952
                private boolean lookedUpWraps;
953

954
                private boolean hWrap;
955

956
                private boolean vWrap;
957

958
                @JsonIgnore
959
                private final Epoch epoch;
960

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

973
                        this.epoch = epochs.getMachineEpoch(id);
2✔
974
                }
2✔
975

976
                private int getArea() {
977
                        return width * height * TRIAD_DEPTH;
2✔
978
                }
979

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

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

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

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

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

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

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

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

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

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

1110
                @Override
1111
                public int getId() {
1112
                        return id;
2✔
1113
                }
1114

1115
                @Override
1116
                public String getName() {
1117
                        return name;
2✔
1118
                }
1119

1120
                @Override
1121
                public Set<String> getTags() {
1122
                        return tags;
2✔
1123
                }
1124

1125
                @Override
1126
                public int getWidth() {
1127
                        return width;
2✔
1128
                }
1129

1130
                @Override
1131
                public int getHeight() {
1132
                        return height;
2✔
1133
                }
1134

1135
                @Override
1136
                public boolean isInService() {
1137
                        return inService;
2✔
1138
                }
1139

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

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

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

1169
                @Override
1170
                public int hashCode() {
1171
                        return id;
×
1172
                }
1173

1174
                @Override
1175
                public String toString() {
1176
                        return "Machine(" + name + ")";
×
1177
                }
1178

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

1195
                @Override
1196
                public boolean isHorizonallyWrapped() {
1197
                        if (!lookedUpWraps) {
×
1198
                                retrieveWraps();
×
1199
                        }
1200
                        return hWrap;
×
1201
                }
1202

1203
                @Override
1204
                public boolean isVerticallyWrapped() {
1205
                        if (!lookedUpWraps) {
×
1206
                                retrieveWraps();
×
1207
                        }
1208
                        return vWrap;
×
1209
                }
1210
        }
1211

1212
        private final class JobCollection implements Jobs {
1213
                @JsonIgnore
1214
                private final Epoch epoch;
1215

1216
                private final List<Job> jobs;
1217

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

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

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

1261
                @Override
1262
                public List<Job> jobs() {
1263
                        return copy(jobs);
×
1264
                }
1265

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

1272
        private final class BoardReportSQL extends AbstractSQL {
2✔
1273
                private final Query findBoardByChip = conn.query(findBoardByJobChip);
2✔
1274

1275
                private final Query findBoardByTriad = conn.query(
2✔
1276
                                findBoardByLogicalCoords);
2✔
1277

1278
                private final Query findBoardPhys = conn.query(
2✔
1279
                                findBoardByPhysicalCoords);
2✔
1280

1281
                private final Query findBoardNet = conn.query(findBoardByIPAddress);
2✔
1282

1283
                private final Update insertReport = conn.update(INSERT_BOARD_REPORT);
2✔
1284

1285
                private final Query getReported = conn.query(getReportedBoards);
2✔
1286

1287
                private final Update setFunctioning = conn.update(
2✔
1288
                                SET_FUNCTIONING_FIELD);
1289

1290
                private final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
2✔
1291

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

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

1314
                private final int id;
1315

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

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

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

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

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

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

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

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

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

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

1377
        private final class JobImpl implements Job {
1378
                @JsonIgnore
1379
                private Epoch epoch;
1380

1381
                private final int id;
1382

1383
                private final int machineId;
1384

1385
                private Integer width;
1386

1387
                private Integer height;
1388

1389
                private Integer depth;
1390

1391
                private JobState state;
1392

1393
                /** If not {@code null}, the ID of the root board of the job. */
1394
                private Integer root;
1395

1396
                private ChipLocation chipRoot;
1397

1398
                private String owner;
1399

1400
                private String keepaliveHost;
1401

1402
                private Instant startTime;
1403

1404
                private Instant keepaliveTime;
1405

1406
                private Instant finishTime;
1407

1408
                private String deathReason;
1409

1410
                private byte[] request;
1411

1412
                private boolean partial;
1413

1414
                private MachineImpl cachedMachine;
1415

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

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

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

1453
                        this.epoch = epochs.getJobsEpoch(id);
2✔
1454
                }
2✔
1455

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

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

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

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

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

1510
                @Override
1511
                public int getId() {
1512
                        return id;
2✔
1513
                }
1514

1515
                @Override
1516
                public JobState getState() {
1517
                        return state;
2✔
1518
                }
1519

1520
                @Override
1521
                public Instant getStartTime() {
1522
                        return startTime;
2✔
1523
                }
1524

1525
                @Override
1526
                public Optional<Instant> getFinishTime() {
1527
                        return Optional.ofNullable(finishTime);
2✔
1528
                }
1529

1530
                @Override
1531
                public Optional<String> getReason() {
1532
                        return Optional.ofNullable(deathReason);
2✔
1533
                }
1534

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

1543
                @Override
1544
                public Instant getKeepaliveTimestamp() {
1545
                        return keepaliveTime;
2✔
1546
                }
1547

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

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

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

1577
                // -------------------------------------------------------------
1578
                // Bad board report handling
1579

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

1596
                                return result;
2✔
1597
                        } catch (ReportRollbackExn e) {
×
1598
                                return e.getMessage();
×
1599
                        }
1600
                }
1601

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

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

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

1723
                // -------------------------------------------------------------
1724

1725
                @Override
1726
                public Optional<ChipLocation> getRootChip() {
1727
                        return Optional.ofNullable(chipRoot);
2✔
1728
                }
1729

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

1738
                @Override
1739
                public Optional<Integer> getWidth() {
1740
                        return Optional.ofNullable(width);
2✔
1741
                }
1742

1743
                @Override
1744
                public Optional<Integer> getHeight() {
1745
                        return Optional.ofNullable(height);
2✔
1746
                }
1747

1748
                @Override
1749
                public Optional<Integer> getDepth() {
1750
                        return Optional.ofNullable(depth);
2✔
1751
                }
1752

1753
                @Override
1754
                public void rememberProxy(ProxyCore proxy) {
1755
                        rememberer.rememberProxyForJob(id, proxy);
×
1756
                }
×
1757

1758
                @Override
1759
                public void forgetProxy(ProxyCore proxy) {
1760
                        rememberer.removeProxyForJob(id, proxy);
×
1761
                }
×
1762

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

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

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

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

1824
                @Override
1825
                public int hashCode() {
1826
                        return id;
×
1827
                }
1828

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

1835
                private final class SubMachineImpl implements SubMachine {
1836
                        /** The machine that this sub-machine is part of. */
1837
                        private final Machine machine;
1838

1839
                        /** The root X coordinate of this sub-machine. */
1840
                        private int rootX;
1841

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

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

1848
                        /** The connection details of this sub-machine. */
1849
                        private List<ConnectionInfo> connections;
1850

1851
                        /** The board locations of this sub-machine. */
1852
                        private List<BoardCoordinates> boards;
1853

1854
                        private List<Integer> boardIds;
1855

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

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

1900
                        @Override
1901
                        public Machine getMachine() {
1902
                                return machine;
2✔
1903
                        }
1904

1905
                        @Override
1906
                        public int getRootX() {
1907
                                return rootX;
2✔
1908
                        }
1909

1910
                        @Override
1911
                        public int getRootY() {
1912
                                return rootY;
2✔
1913
                        }
1914

1915
                        @Override
1916
                        public int getRootZ() {
1917
                                return rootZ;
2✔
1918
                        }
1919

1920
                        @Override
1921
                        public int getWidth() {
1922
                                return width;
2✔
1923
                        }
1924

1925
                        @Override
1926
                        public int getHeight() {
1927
                                return height;
2✔
1928
                        }
1929

1930
                        @Override
1931
                        public int getDepth() {
1932
                                return depth;
2✔
1933
                        }
1934

1935
                        @Override
1936
                        public List<ConnectionInfo> getConnections() {
1937
                                return connections;
2✔
1938
                        }
1939

1940
                        @Override
1941
                        public List<BoardCoordinates> getBoards() {
1942
                                return boards;
2✔
1943
                        }
1944

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

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

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

1977
                private final String machineName;
1978

1979
                private final int machineWidth;
1980

1981
                private final int machineHeight;
1982

1983
                private final ChipLocation chip;
1984

1985
                private final ChipLocation boardChip;
1986

1987
                private final BoardCoordinates logical;
1988

1989
                private final BoardPhysicalCoordinates physical;
1990

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

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

2016
                @Override
2017
                public ChipLocation getBoardChip() {
2018
                        return boardChip;
2✔
2019
                }
2020

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

2034
                @Override
2035
                public String getMachine() {
2036
                        return machineName;
2✔
2037
                }
2038

2039
                @Override
2040
                public BoardCoordinates getLogical() {
2041
                        return logical;
2✔
2042
                }
2043

2044
                @Override
2045
                public BoardPhysicalCoordinates getPhysical() {
2046
                        return physical;
2✔
2047
                }
2048

2049
                @Override
2050
                public ChipLocation getChip() {
2051
                        return chip;
2✔
2052
                }
2053

2054
                @Override
2055
                public Job getJob() {
2056
                        return job;
2✔
2057
                }
2058
        }
2059

2060
        static class PartialJobException extends IllegalStateException {
2061
                private static final long serialVersionUID = 2997856394666135483L;
2062

2063
                PartialJobException() {
2064
                        super("partial job only");
×
2065
                }
×
2066
        }
2067
}
2068

2069
class ReportRollbackExn extends RuntimeException {
2070
        private static final long serialVersionUID = 1L;
2071

2072
        @FormatMethod
2073
        ReportRollbackExn(String msg, Object... args) {
2074
                super(format(msg, args));
×
2075
        }
×
2076

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

2083
abstract class GroupsException extends RuntimeException {
2084
        private static final long serialVersionUID = 6607077117924279611L;
2085

2086
        GroupsException(String message) {
2087
                super(message);
×
2088
        }
×
2089

2090
        GroupsException(String message, Throwable cause) {
2091
                super(message, cause);
×
2092
        }
×
2093
}
2094

2095
class NoSuchGroupException extends GroupsException {
2096
        private static final long serialVersionUID = 5193818294198205503L;
2097

2098
        @FormatMethod
2099
        NoSuchGroupException(String msg, Object... args) {
2100
                super(format(msg, args));
×
2101
        }
×
2102
}
2103

2104
class MultipleGroupsException extends GroupsException {
2105
        private static final long serialVersionUID = 6284332340565334236L;
2106

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

© 2026 Coveralls, Inc