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

SpiNNakerManchester / JavaSpiNNaker / 15206883803

23 May 2025 09:20AM UTC coverage: 37.541% (-0.8%) from 38.295%
15206883803

push

github

rowleya
Merge branch 'master' into doc_fix

114 of 152 new or added lines in 10 files covered. (75.0%)

231 existing lines in 4 files now uncovered.

9065 of 24147 relevant lines covered (37.54%)

1.09 hits per line

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

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

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

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

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

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

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

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

115
        private static final Logger log = getLogger(Spalloc.class);
3✔
116

117
        @Autowired
118
        private PowerController powerController;
119

120
        @Autowired
121
        private Epochs epochs;
122

123
        @Autowired
124
        private QuotaManager quotaManager;
125

126
        @Autowired
127
        private ReportMailSender emailSender;
128

129
        @Autowired
130
        private AllocatorProperties props;
131

132
        @Autowired
133
        private JobObjectRememberer rememberer;
134

135
        @Autowired
136
        private AllocatorTask allocator;
137

138
        @GuardedBy("this")
3✔
139
        private transient Map<String, List<BoardCoords>> downBoardsCache =
140
                        new HashMap<>();
141

142
        @GuardedBy("this")
3✔
143
        private transient Map<String, List<DownLink>> downLinksCache =
144
                        new HashMap<>();
145

146
        private boolean emergencyStop = false;
3✔
147

148
        @Override
149
        public Map<String, Machine> getMachines(boolean allowOutOfService) {
150
                return executeRead(c -> getMachines(c, allowOutOfService));
3✔
151
        }
152

153
        private Map<String, Machine> getMachines(Connection conn,
154
                        boolean allowOutOfService) {
155
                try (var listMachines = conn.query(GET_ALL_MACHINES)) {
3✔
156
                        return Row.stream(listMachines.call(
3✔
157
                                        row -> new MachineImpl(conn, row), allowOutOfService))
3✔
158
                                        .toMap(Machine::getName, (m) -> m);
3✔
159
                }
160
        }
161

162
        private final class ListMachinesSQL extends AbstractSQL {
3✔
163
                private final Query listMachines = conn.query(GET_ALL_MACHINES);
3✔
164

165
                private final Query countMachineThings =
3✔
166
                                conn.query(COUNT_MACHINE_THINGS);
3✔
167

168
                private final Query getTags = conn.query(GET_TAGS);
3✔
169

170
                @Override
171
                public void close() {
172
                        listMachines.close();
3✔
173
                        countMachineThings.close();
3✔
174
                        getTags.close();
3✔
175
                        super.close();
3✔
176
                }
3✔
177

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

197
        @Override
198
        public List<MachineListEntryRecord>
199
                        listMachines(boolean allowOutOfService) {
200
                try (var sql = new ListMachinesSQL()) {
3✔
201
                        return sql.transactionRead(
3✔
202
                                        () -> sql.listMachines.call(
3✔
203
                                                        sql::makeMachineListEntryRecord,
3✔
204
                                                        allowOutOfService));
3✔
205
                }
206
        }
207

208
        @Override
209
        public Optional<Machine> getMachine(String name,
210
                        boolean allowOutOfService) {
211
                return executeRead(
3✔
212
                                conn -> getMachine(name, allowOutOfService, conn).map(m -> m));
3✔
213
        }
214

215
        private Optional<MachineImpl> getMachine(int id, boolean allowOutOfService,
216
                        Connection conn) {
217
                try (var idMachine = conn.query(GET_MACHINE_BY_ID)) {
3✔
218
                        return idMachine.call1(row -> new MachineImpl(conn, row),
3✔
219
                                        id, allowOutOfService);
3✔
220
                }
221
        }
222

223
        private Optional<MachineImpl> getMachine(String name,
224
                        boolean allowOutOfService, Connection conn) {
225
                try (var namedMachine = conn.query(GET_NAMED_MACHINE)) {
3✔
226
                        return namedMachine.call1(row -> new MachineImpl(conn, row),
3✔
227
                                        name, allowOutOfService);
3✔
228
                }
229
        }
230

231
        private final class DescribeMachineSQL extends AbstractSQL {
3✔
232
                final Query namedMachine = conn.query(GET_NAMED_MACHINE);
3✔
233

234
                final Query countMachineThings = conn.query(COUNT_MACHINE_THINGS);
3✔
235

236
                final Query getTags = conn.query(GET_TAGS);
3✔
237

238
                final Query getJobs = conn.query(GET_MACHINE_JOBS);
3✔
239

240
                final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
3✔
241

242
                final Query getLive = conn.query(GET_LIVE_BOARDS);
3✔
243

244
                final Query getDead = conn.query(GET_DEAD_BOARDS);
3✔
245

246
                final Query getQuota = conn.query(GET_USER_QUOTA);
3✔
247

248
                @Override
249
                public void close() {
250
                        namedMachine.close();
3✔
251
                        countMachineThings.close();
3✔
252
                        getTags.close();
3✔
253
                        getJobs.close();
3✔
254
                        getCoords.close();
3✔
255
                        getLive.close();
3✔
256
                        getDead.close();
3✔
257
                        getQuota.close();
3✔
258
                        super.close();
3✔
259
                }
3✔
260
        }
261

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

288
        private static MachineDescription getBasicMachineInfo(Row row) {
289
                var md = new MachineDescription();
3✔
290
                md.setId(row.getInt("machine_id"));
3✔
291
                md.setName(row.getString("machine_name"));
3✔
292
                md.setWidth(row.getInt("width"));
3✔
293
                md.setHeight(row.getInt("height"));
3✔
294
                return md;
3✔
295
        }
296

297
        private static JobInfo getMachineJobInfo(Permit permit, Query getCoords,
298
                        Row row) {
299
                int jobId = row.getInt("job_id");
3✔
300
                var mayUnveil = permit.unveilFor(row.getString("owner_name"));
3✔
301
                var owner = mayUnveil ? row.getString("owner_name") : null;
3✔
302

303
                var ji = new JobInfo();
3✔
304
                ji.setId(jobId);
3✔
305
                ji.setOwner(owner);
3✔
306
                ji.setBoards(
3✔
307
                                getCoords.call(r -> new BoardCoords(r, !mayUnveil), jobId));
3✔
308
                return ji;
3✔
309
        }
310

311
        @Override
312
        public Jobs getJobs(boolean deleted, int limit, int start) {
313
                return executeRead(conn -> {
3✔
314
                        if (deleted) {
3✔
315
                                try (var jobs = conn.query(GET_JOB_IDS)) {
3✔
316
                                        return new JobCollection(
3✔
317
                                                        jobs.call(this::makeJob, limit, start));
3✔
318
                                }
319
                        } else {
320
                                try (var jobs = conn.query(GET_LIVE_JOB_IDS)) {
3✔
321
                                        return new JobCollection(
3✔
322
                                                        jobs.call(this::makeJob, limit, start));
3✔
323
                                }
324
                        }
325
                });
326
        }
327

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

343
        @Override
344
        public List<JobListEntryRecord> listJobs(Permit permit) {
345
                return executeRead(conn -> {
3✔
346
                        try (var listLiveJobs = conn.query(LIST_LIVE_JOBS);
3✔
347
                                        var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
3✔
348
                                        var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
3✔
349
                                return listLiveJobs.call(row -> makeJobListEntryRecord(permit,
3✔
350
                                                countPoweredBoards, getCoords, row));
351
                        }
352
                });
353
        }
354

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

378
        @Override
379
        @PostFilter(MAY_SEE_JOB_DETAILS)
380
        public Optional<Job> getJob(Permit permit, int id) {
381
                return executeRead(conn -> getJob(id, conn).map(j -> (Job) j));
3✔
382
        }
383

384
        private Optional<JobImpl> getJob(int id, Connection conn) {
385
                try (var s = conn.query(GET_JOB)) {
3✔
386
                        return s.call1(row -> new JobImpl(conn, row), id);
3✔
387
                }
388
        }
389

390
        @Override
391
        @PostFilter(MAY_SEE_JOB_DETAILS)
392
        public Optional<JobDescription> getJobInfo(Permit permit, int id) {
393
                return executeRead(conn -> {
3✔
394
                        try (var s = conn.query(GET_JOB);
3✔
395
                                        var chipDimensions = conn.query(GET_JOB_CHIP_DIMENSIONS);
3✔
396
                                        var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
3✔
397
                                        var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
3✔
398
                                return s.call1(row -> jobDescription(id, row,
3✔
399
                                                chipDimensions, countPoweredBoards, getCoords), id);
3✔
400
                        }
401
                });
402
        }
403

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

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

451
                        var m = selectMachine(conn, descriptor, machineName, tags);
3✔
452
                        if (!m.isPresent()) {
3✔
453
                                throw new IllegalArgumentException(
×
454
                                                "no machine available which matches allocation "
455
                                                + "request");
456
                        }
457
                        var machine = m.orElseThrow();
3✔
458

459
                        var id = insertJob(conn, machine, user, group, keepaliveInterval,
3✔
460
                                        req);
461
                        if (!id.isPresent()) {
3✔
462
                                throw new RuntimeException("failed to create job");
×
463
                        }
464
                        int jobId = id.orElseThrow();
3✔
465

466
                        var scale = props.getPriorityScale();
3✔
467

468
                        if (machine.getArea() < descriptor.getArea()) {
3✔
469
                                throw new IllegalArgumentException(
×
470
                                                "request cannot fit on machine");
471
                        }
472

473
                        // Ask the allocator engine to do the allocation
474
                        int numBoards = descriptor.visit(new CreateVisitor<Integer>() {
3✔
475
                                @Override
476
                                public Integer numBoards(CreateNumBoards nb) {
477
                                        try (var insertReq = conn.update(INSERT_REQ_N_BOARDS)) {
3✔
478
                                                insertReq.call(jobId, nb.numBoards, nb.maxDead,
3✔
479
                                                                (int) (nb.getArea() * scale.getSize()));
3✔
480
                                        }
481
                                        return nb.numBoards;
3✔
482
                                }
483

484
                                @Override
485
                                public Integer dimensions(CreateDimensions d) {
486
                                        try (var insertReq = conn.update(INSERT_REQ_SIZE)) {
3✔
487
                                                insertReq.call(jobId, d.width, d.height, d.maxDead,
3✔
488
                                                                (int) (d.getArea() * scale.getDimensions()));
3✔
489
                                        }
490
                                        return max(1, d.getArea() - d.maxDead);
3✔
491
                                }
492

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

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

529
                        // DB now changed; can report success
530
                        JobLifecycle.log.info(
3✔
531
                                        "created job {} on {} for {} asking for {} board(s)", jobId,
3✔
532
                                        machine.name, owner, numBoards);
3✔
533

534
                        allocator.scheduleAllocateNow();
3✔
535
                        return getJob(jobId, conn).map(ji -> (Job) ji).orElseThrow(
3✔
536
                                        () -> new RuntimeException("Error creating job!"));
×
537
                });
538
        }
539

540
        @Override
541
        public Job createJob(String owner, CreateDescriptor descriptor,
542
                        String machineName, List<String> tags, Duration keepaliveInterval,
543
                        byte[] originalRequest) {
544
                return execute(conn -> createJobInGroup(
3✔
545
                                owner, getOnlyGroup(conn, owner), descriptor, machineName,
3✔
546
                                tags, keepaliveInterval, originalRequest));
547
        }
548

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

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

562
                quotaManager.associateNMPISession(job.getId(), session.getId(),
×
563
                                quotaUnits);
564

565
                // Return the job created
566
                return job;
×
567
        }
568

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

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

584
                quotaManager.associateNMPIJob(job.getId(), nmpiJobId,
×
585
                                quotaDetails.quotaUnits);
586

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

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

615
        private String getOnlyGroup(Connection conn, String user) {
616
                try (var listGroups = conn.query(GET_GROUP_NAMES_OF_USER)) {
3✔
617
                        // No name given; need to guess.
618
                        var groups = listGroups.call(row -> row.getString("group_name"),
3✔
619
                                        user);
620
                        if (groups.size() > 1) {
3✔
621
                                throw new NoSuchGroupException(
×
622
                                                "User is a member of more than one group, so the group"
623
                                                + " must be selected in the request");
624
                        }
625
                        if (groups.size() == 0) {
3✔
626
                                throw new NoSuchGroupException(
×
627
                                                "User is not a member of any group!");
628
                        }
629
                        return groups.get(0);
3✔
630
                }
631
        }
632

633
        private static Optional<Integer> getUser(Connection conn, String userName) {
634
                try (var getUser = conn.query(GET_USER_ID)) {
3✔
635
                        return getUser.call1(integer("user_id"), userName);
3✔
636
                }
637
        }
638

639
        private class BoardLocated {
640
                int boardId;
641

642
                int z;
643

644
                BoardLocated(Row row) {
3✔
645
                        boardId = row.getInt("board_id");
3✔
646
                        z = row.getInt("z");
3✔
647
                }
3✔
648
        }
649

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

696
        private static Optional<Integer> insertJob(Connection conn, MachineImpl m,
697
                        int owner, int group, Duration keepaliveInterval, byte[] req) {
698
                try (var makeJob = conn.update(INSERT_JOB)) {
3✔
699
                        return makeJob.key(m.id, owner, group, keepaliveInterval, req);
3✔
700
                }
701
        }
702

703
        private Optional<MachineImpl> selectMachine(Connection conn,
704
                        CreateDescriptor descriptor, String machineName,
705
                        List<String> tags) {
706
                if (nonNull(machineName)) {
3✔
707
                        var m = getMachine(machineName, false, conn);
3✔
708
                        if (m.isPresent() && isAllocPossible(conn, descriptor, m.get())) {
3✔
709
                                return m;
3✔
710
                        }
711
                        return Optional.empty();
×
712
                }
713

714
                if (!tags.isEmpty()) {
×
715
                        for (var m : getMachines(conn, false).values()) {
×
716
                                var mi = (MachineImpl) m;
×
717
                                if (mi.tags.containsAll(tags)
×
718
                                                && isAllocPossible(conn, descriptor, mi)) {
×
719
                                        return Optional.of(mi);
×
720
                                }
721
                        }
×
722
                }
723
                return Optional.empty();
×
724
        }
725

726
        private boolean isAllocPossible(final Connection conn,
727
                        final CreateDescriptor descriptor,
728
                        final MachineImpl m) {
729
                return descriptor.visit(new CreateVisitor<Boolean>() {
3✔
730
                        @Override
731
                        public Boolean numBoards(CreateNumBoards nb) {
732
                                try (var getNBoards = conn.query(COUNT_FUNCTIONING_BOARDS)) {
3✔
733
                                        var numBoards = getNBoards.call1(integer("c"), m.id)
3✔
734
                                                        .orElseThrow();
3✔
735
                                        return numBoards >= nb.numBoards;
3✔
736
                                }
737
                        }
738

739
                        @Override
740
                        public Boolean dimensions(CreateDimensions d) {
741
                                try (var checkPossible = conn.query(checkRectangle)) {
3✔
742
                                        return checkPossible.call1((r) -> true, d.width, d.height,
3✔
743
                                                        m.id, d.maxDead).isPresent();
3✔
744
                                }
745
                        }
746

747
                        @Override
748
                        public Boolean dimensionsAt(CreateDimensionsAt da) {
749
                                try (var checkPossible = conn.query(checkRectangleAt)) {
3✔
750
                                        int board = locateBoard(conn, m.name, da, true);
3✔
751
                                        return checkPossible.call1((r) -> true, board,
3✔
752
                                                        da.width, da.height, m.id, da.maxDead).isPresent();
3✔
753
                                } catch (IllegalArgumentException e) {
×
754
                                        // This means the board doesn't exist on the given machine
755
                                        return false;
×
756
                                }
757
                        }
758

759
                        @Override
760
                        public Boolean board(CreateBoard b) {
761
                                try (var check = conn.query(CHECK_LOCATION)) {
3✔
762
                                        int board = locateBoard(conn, m.name, b, false);
3✔
763
                                        return check.call1((r) -> true, m.id, board).isPresent();
3✔
764
                                } catch (IllegalArgumentException e) {
×
765
                                        // This means the board doesn't exist on the given machine
766
                                        return false;
×
767
                                }
768
                        }
769
                });
770
        }
771

772
        @Override
773
        public void purgeDownCache() {
774
                synchronized (this) {
×
775
                        downBoardsCache.clear();
×
776
                        downLinksCache.clear();
×
777
                }
×
778
        }
×
779

780
        private static String mergeDescription(HasChipLocation coreLocation,
781
                        String description) {
782
                if (isNull(description)) {
3✔
783
                        description = "<null>";
×
784
                }
785
                if (coreLocation instanceof HasCoreLocation) {
3✔
786
                        var loc = (HasCoreLocation) coreLocation;
×
787
                        description += format(" (at core %d of chip %s)", loc.getP(),
×
788
                                        loc.asChipLocation());
×
789
                } else if (nonNull(coreLocation)) {
3✔
790
                        description +=
×
791
                                        format(" (at chip %s)", coreLocation.asChipLocation());
×
792
                }
793
                return description;
3✔
794
        }
795

796
        private class Problem {
797
                int boardId;
798

799
                Integer jobId;
800

801
                Problem(Row row) {
3✔
802
                        boardId = row.getInt("board_id");
3✔
803
                        jobId = row.getInt("job_id");
3✔
804
                }
3✔
805
        }
806

807
        @Override
808
        public void reportProblem(String address, HasChipLocation coreLocation,
809
                        String description, Permit permit) {
810
                try (var sql = new BoardReportSQL()) {
3✔
811
                        var desc = mergeDescription(coreLocation, description);
3✔
812
                        var email = sql.transaction(() -> {
3✔
813
                                var machines = getMachines(sql.getConnection(), true).values();
3✔
814
                                for (var m : machines) {
3✔
815
                                        var mail = sql.findBoardNet.call1(
3✔
816
                                                        Problem::new, m.getId(), address)
3✔
817
                                                        .flatMap(prob -> reportProblem(prob, desc, permit,
3✔
818
                                                                        sql));
819
                                        if (mail.isPresent()) {
3✔
820
                                                return mail;
×
821
                                        }
822
                                }
3✔
823
                                return Optional.empty();
3✔
824
                        });
825
                        // Outside the transaction!
826
                        email.ifPresent(emailSender::sendServiceMail);
3✔
827
                } catch (ReportRollbackExn e) {
×
828
                        log.warn("failed to handle problem report", e);
×
829
                }
3✔
830
        }
3✔
831

832
        private Optional<EmailBuilder> reportProblem(Problem problem,
833
                        String description,        Permit permit, BoardReportSQL sql) {
834
                var email = new EmailBuilder(problem.jobId);
3✔
835
                email.header(description, 1, permit.name);
3✔
836
                int userId = getUser(sql.getConnection(), permit.name).orElseThrow(
3✔
837
                                () -> new ReportRollbackExn("no such user: %s", permit.name));
×
838
                sql.insertReport.key(problem.boardId, problem.jobId,
3✔
839
                                description, userId).ifPresent(email::issue);
3✔
840
                return takeBoardsOutOfService(sql, email).map(acted -> {
3✔
841
                        email.footer(acted);
×
842
                        return email;
×
843
                });
844
        }
845

846
        private class Reported {
847
                int boardId;
848

849
                int x;
850

851
                int y;
852

853
                int z;
854

855
                String address;
856

857
                int numReports;
858

859
                Reported(Row row) {
×
860
                        boardId = row.getInt("board_id");
×
861
                        x = row.getInt("x");
×
862
                        y = row.getInt("y");
×
863
                        z = row.getInt("z");
×
864
                        address = row.getString("address");
×
865
                        numReports = row.getInt("numReports");
×
866
                }
×
867

868
        }
869

870
        /**
871
         * Take boards out of service if they've been reported frequently enough.
872
         *
873
         * @param sql
874
         *            How to touch the DB
875
         * @param email
876
         *            The email we are building.
877
         * @return The number of boards taken out of service
878
         */
879
        private Optional<Integer> takeBoardsOutOfService(BoardReportSQL sql,
880
                        EmailBuilder email) {
881
                int acted = 0;
3✔
882
                for (var report : sql.getReported.call(Reported::new,
3✔
883
                                props.getReportActionThreshold())) {
3✔
884
                        if (sql.setFunctioning.call(false, report.boardId) > 0) {
×
885
                                email.serviceActionDone(report);
×
886
                                acted++;
×
887
                        }
888
                }
×
889
                if (acted > 0) {
3✔
890
                        purgeDownCache();
×
891
                }
892
                return acted > 0 ? Optional.of(acted) : Optional.empty();
3✔
893
        }
894

895
        private static DownLink makeDownLinkFromRow(Row row) {
896
                // Non-standard column names to reduce number of queries
897
                var board1 = new BoardCoords(row.getInt("board_1_x"),
×
898
                                row.getInt("board_1_y"), row.getInt("board_1_z"),
×
899
                                row.getInt("board_1_c"), row.getInt("board_1_f"),
×
900
                                row.getInteger("board_1_b"), row.getString("board_1_addr"));
×
901
                var board2 = new BoardCoords(row.getInt("board_2_x"),
×
902
                                row.getInt("board_2_y"), row.getInt("board_2_z"),
×
903
                                row.getInt("board_2_c"), row.getInt("board_2_f"),
×
904
                                row.getInteger("board_2_b"), row.getString("board_2_addr"));
×
905
                return new DownLink(board1, row.getEnum("dir_1", Direction.class),
×
906
                                board2, row.getEnum("dir_2", Direction.class));
×
907
        }
908

909
        public void emergencyStop(String commandCode) {
NEW
910
                if (!commandCode.equals(props.getEmergencyStopCommandCode())) {
×
NEW
911
                        throw new IllegalArgumentException("Invalid emergency stop code");
×
912
                }
NEW
913
                allocator.emergencyStop();
×
NEW
914
                emergencyStop = true;
×
NEW
915
                log.warn("Emergency stop requested!");
×
NEW
916
        }
×
917

918
        private class MachineImpl implements Machine {
919
                private final int id;
920

921
                private final boolean inService;
922

923
                private final String name;
924

925
                private final Set<String> tags;
926

927
                private final int width;
928

929
                private final int height;
930

931
                private boolean lookedUpWraps;
932

933
                private boolean hWrap;
934

935
                private boolean vWrap;
936

937
                @JsonIgnore
938
                private final Epoch epoch;
939

940
                MachineImpl(Connection conn, Row rs) {
3✔
941
                        id = rs.getInt("machine_id");
3✔
942
                        name = rs.getString("machine_name");
3✔
943
                        width = rs.getInt("width");
3✔
944
                        height = rs.getInt("height");
3✔
945
                        inService = rs.getBoolean("in_service");
3✔
946
                        lookedUpWraps = false;
3✔
947
                        try (var getTags = conn.query(GET_TAGS)) {
3✔
948
                                tags = Row.stream(copy(getTags.call(string("tag"), id)))
3✔
949
                                                .toSet();
3✔
950
                        }
951

952
                        this.epoch = epochs.getMachineEpoch(id);
3✔
953
                }
3✔
954

955
                private int getArea() {
956
                        return width * height * TRIAD_DEPTH;
3✔
957
                }
958

959
                @Override
960
                public boolean waitForChange(Duration timeout) {
961
                        if (isNull(epoch)) {
3✔
962
                                log.info("Machine {} epoch is null!", id);
×
963
                                return true;
×
964
                        }
965
                        try {
966
                                log.info("Waiting for change in epoch for {}", id);
3✔
967
                                return epoch.waitForChange(timeout);
×
968
                        } catch (InterruptedException interrupted) {
3✔
969
                                log.info("Interrupted waiting for change on {}", id);
3✔
970
                                return false;
3✔
971
                        }
972
                }
973

974
                @Override
975
                public Optional<BoardLocation> getBoardByChip(HasChipLocation chip) {
976
                        try (var conn = getConnection();
3✔
977
                                        var findBoard = conn.query(findBoardByGlobalChip)) {
3✔
978
                                return conn.transaction(false,
3✔
979
                                                () -> findBoard.call1(
3✔
980
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
981
                                                                chip.getX(), chip.getY()));
3✔
982
                        }
983
                }
984

985
                @Override
986
                public Optional<BoardLocation> getBoardByPhysicalCoords(
987
                                PhysicalCoords coords) {
988
                        try (var conn = getConnection();
3✔
989
                                        var findBoard = conn.query(findBoardByPhysicalCoords)) {
3✔
990
                                return conn.transaction(false,
3✔
991
                                                () -> findBoard.call1(
3✔
992
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
993
                                                                coords.c, coords.f, coords.b));
3✔
994
                        }
995
                }
996

997
                @Override
998
                public Optional<BoardLocation> getBoardByLogicalCoords(
999
                                TriadCoords coords) {
1000
                        try (var conn = getConnection();
3✔
1001
                                        var findBoard = conn.query(findBoardByLogicalCoords)) {
3✔
1002
                                return conn.transaction(false,
3✔
1003
                                                () -> findBoard.call1(
3✔
1004
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
1005
                                                                coords.x, coords.y, coords.z));
3✔
1006
                        }
1007
                }
1008

1009
                @Override
1010
                public Optional<BoardLocation> getBoardByIPAddress(String address) {
1011
                        try (var conn = getConnection();
3✔
1012
                                        var findBoard = conn.query(findBoardByIPAddress)) {
3✔
1013
                                return conn.transaction(false,
3✔
1014
                                                () -> findBoard.call1(
3✔
1015
                                                                row -> new BoardLocationImpl(row, this), id,
3✔
1016
                                                                address));
1017
                        }
1018
                }
1019

1020
                @Override
1021
                public String getRootBoardBMPAddress() {
1022
                        try (var conn = getConnection();
3✔
1023
                                        var rootBMPaddr = conn.query(GET_ROOT_BMP_ADDRESS)) {
3✔
1024
                                return conn.transaction(false, () -> rootBMPaddr.call1(
3✔
1025
                                                string("address"), id).orElse(null));
3✔
1026
                        }
1027
                }
1028

1029
                @Override
1030
                public List<Integer> getBoardNumbers() {
1031
                        try (var conn = getConnection();
3✔
1032
                                        var boardNumbers = conn.query(GET_BOARD_NUMBERS)) {
3✔
1033
                                return conn.transaction(false, () -> boardNumbers.call(
3✔
1034
                                                integer("board_num"), id));
3✔
1035
                        }
1036
                }
1037

1038
                @Override
1039
                public List<BoardCoords> getDeadBoards() {
1040
                        // Assume that the list doesn't change for the duration of this obj
1041
                        synchronized (Spalloc.this) {
3✔
1042
                                var down = downBoardsCache.get(name);
3✔
1043
                                if (nonNull(down)) {
3✔
1044
                                        return copy(down);
3✔
1045
                                }
1046
                        }
3✔
1047
                        try (var conn = getConnection();
3✔
1048
                                        var boardNumbers = conn.query(GET_DEAD_BOARDS)) {
3✔
1049
                                var downBoards = conn.transaction(false,
3✔
1050
                                                () -> boardNumbers.call(
3✔
1051
                                                                row -> new BoardCoords(row, false), id));
3✔
1052
                                synchronized (Spalloc.this) {
3✔
1053
                                        downBoardsCache.putIfAbsent(name, downBoards);
3✔
1054
                                }
3✔
1055
                                return copy(downBoards);
3✔
1056
                        }
1057
                }
1058

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

1079
                @Override
1080
                public List<Integer> getAvailableBoards() {
1081
                        try (var conn = getConnection();
3✔
1082
                                        var boardNumbers = conn
3✔
1083
                                                        .query(GET_AVAILABLE_BOARD_NUMBERS)) {
3✔
1084
                                return conn.transaction(false, () -> boardNumbers.call(
3✔
1085
                                                integer("board_num"), id));
3✔
1086
                        }
1087
                }
1088

1089
                @Override
1090
                public int getId() {
1091
                        return id;
3✔
1092
                }
1093

1094
                @Override
1095
                public String getName() {
1096
                        return name;
3✔
1097
                }
1098

1099
                @Override
1100
                public Set<String> getTags() {
1101
                        return tags;
3✔
1102
                }
1103

1104
                @Override
1105
                public int getWidth() {
1106
                        return width;
3✔
1107
                }
1108

1109
                @Override
1110
                public int getHeight() {
1111
                        return height;
3✔
1112
                }
1113

1114
                @Override
1115
                public boolean isInService() {
1116
                        return inService;
3✔
1117
                }
1118

1119
                @Override
1120
                public String getBMPAddress(BMPCoords bmp) {
1121
                        try (var conn = getConnection();
3✔
1122
                                        var bmpAddr = conn.query(GET_BMP_ADDRESS)) {
3✔
1123
                                return conn.transaction(false,
3✔
1124
                                                () -> bmpAddr
1125
                                                                .call1(string("address"), id, bmp.getCabinet(),
3✔
1126
                                                                                bmp.getFrame()).orElse(null));
3✔
1127
                        }
1128
                }
1129

1130
                @Override
1131
                public List<Integer> getBoardNumbers(BMPCoords bmp) {
1132
                        try (var conn = getConnection();
3✔
1133
                                        var boardNumbers = conn.query(GET_BMP_BOARD_NUMBERS)) {
3✔
1134
                                return conn.transaction(false,
3✔
1135
                                                () -> boardNumbers
3✔
1136
                                                                .call(integer("board_num"), id,
3✔
1137
                                                                                bmp.getCabinet(), bmp.getFrame()));
3✔
1138
                        }
1139
                }
1140

1141
                @Override
1142
                public boolean equals(Object other) {
1143
                        // Equality is defined exactly by the database ID
1144
                        return (other instanceof MachineImpl)
×
1145
                                        && (id == ((MachineImpl) other).id);
1146
                }
1147

1148
                @Override
1149
                public int hashCode() {
1150
                        return id;
×
1151
                }
1152

1153
                @Override
1154
                public String toString() {
1155
                        return "Machine(" + name + ")";
×
1156
                }
1157

1158
                private void retrieveWraps() {
1159
                        try (var conn = getConnection();
×
1160
                                        var getWraps = conn.query(GET_MACHINE_WRAPS)) {
×
1161
                                /*
1162
                                 * No locking; not too bothered which thread asks as result will
1163
                                 * be the same either way
1164
                                 */
1165
                                lookedUpWraps =
×
1166
                                                conn.transaction(false, () -> getWraps.call1(rs -> {
×
1167
                                                        hWrap = rs.getBoolean("horizontal_wrap");
×
1168
                                                        vWrap = rs.getBoolean("vertical_wrap");
×
1169
                                                        return true;
×
1170
                                                }, id)).orElse(false);
×
1171
                        }
1172
                }
×
1173

1174
                @Override
1175
                public boolean isHorizonallyWrapped() {
1176
                        if (!lookedUpWraps) {
×
1177
                                retrieveWraps();
×
1178
                        }
1179
                        return hWrap;
×
1180
                }
1181

1182
                @Override
1183
                public boolean isVerticallyWrapped() {
1184
                        if (!lookedUpWraps) {
×
1185
                                retrieveWraps();
×
1186
                        }
1187
                        return vWrap;
×
1188
                }
1189
        }
1190

1191
        private final class JobCollection implements Jobs {
1192
                @JsonIgnore
1193
                private final Epoch epoch;
1194

1195
                private final List<Job> jobs;
1196

1197
                private JobCollection(List<Job> jobs) {
3✔
1198
                        this.jobs = jobs;
3✔
1199
                        if (jobs.isEmpty()) {
3✔
1200
                                epoch = null;
3✔
1201
                        } else {
1202
                                epoch = epochs.getJobsEpoch(
3✔
1203
                                                jobs.stream().map(Job::getId).collect(toList()));
3✔
1204
                        }
1205
                }
3✔
1206

1207
                @Override
1208
                public boolean waitForChange(Duration timeout) {
1209
                        if (isNull(epoch)) {
×
1210
                                return true;
×
1211
                        }
1212
                        try {
1213
                                return epoch.waitForChange(timeout);
×
1214
                        } catch (InterruptedException interrupted) {
×
1215
                                currentThread().interrupt();
×
1216
                                return false;
×
1217
                        }
1218
                }
1219

1220
                /**
1221
                 * Get the set of jobs changed.
1222
                 *
1223
                 * @param timeout
1224
                 *            The timeout to wait for until something happens.
1225
                 * @return The set of changed job identifiers.
1226
                 */
1227
                @Override
1228
                public Collection<Integer> getChanged(Duration timeout) {
1229
                        if (isNull(epoch)) {
3✔
1230
                                return jobs.stream().map(Job::getId).collect(toSet());
3✔
1231
                        }
1232
                        try {
1233
                                return epoch.getChanged(timeout);
×
1234
                        } catch (InterruptedException interrupted) {
×
1235
                                currentThread().interrupt();
×
1236
                                return jobs.stream().map(Job::getId).collect(toSet());
×
1237
                        }
1238
                }
1239

1240
                @Override
1241
                public List<Job> jobs() {
1242
                        return copy(jobs);
×
1243
                }
1244

1245
                @Override
1246
                public List<Integer> ids() {
1247
                        return jobs.stream().map(Job::getId).collect(toList());
3✔
1248
                }
1249
        }
1250

1251
        private final class BoardReportSQL extends AbstractSQL {
3✔
1252
                final Query findBoardByChip = conn.query(findBoardByJobChip);
3✔
1253

1254
                final Query findBoardByTriad = conn.query(findBoardByLogicalCoords);
3✔
1255

1256
                final Query findBoardPhys = conn.query(findBoardByPhysicalCoords);
3✔
1257

1258
                final Query findBoardNet = conn.query(findBoardByIPAddress);
3✔
1259

1260
                final Update insertReport = conn.update(INSERT_BOARD_REPORT);
3✔
1261

1262
                final Query getReported = conn.query(getReportedBoards);
3✔
1263

1264
                final Update setFunctioning = conn.update(SET_FUNCTIONING_FIELD);
3✔
1265

1266
                final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
3✔
1267

1268
                @Override
1269
                public void close() {
1270
                        findBoardByChip.close();
3✔
1271
                        findBoardByTriad.close();
3✔
1272
                        findBoardPhys.close();
3✔
1273
                        findBoardNet.close();
3✔
1274
                        insertReport.close();
3✔
1275
                        getReported.close();
3✔
1276
                        setFunctioning.close();
3✔
1277
                        getNamedMachine.close();
3✔
1278
                        super.close();
3✔
1279
                }
3✔
1280
        }
1281

1282
        /** Used to assemble an issue-report email for sending. */
1283
        private static final class EmailBuilder {
1284
                /**
1285
                 * More efficient than several String.format() calls, and much clearer
1286
                 * than a mess of direct {@link StringBuilder} calls!
1287
                 */
1288
                private final Formatter b = new Formatter(Locale.UK);
3✔
1289

1290
                private final int id;
1291

1292
                /**
1293
                 * @param id
1294
                 *            The job ID
1295
                 */
1296
                EmailBuilder(int id) {
3✔
1297
                        this.id = id;
3✔
1298
                }
3✔
1299

1300
                void header(String issue, int numBoards, String who) {
1301
                        b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
3✔
1302
                                        numBoards, who);
3✔
1303
                }
3✔
1304

1305
                void chip(ReportedBoard board) {
1306
                        b.format("\tBoard for job (%d) chip %s\n", //
×
1307
                                        id, board.chip);
×
1308
                }
×
1309

1310
                void triad(ReportedBoard board) {
1311
                        b.format("\tBoard for job (%d) board (X:%d,Y:%d,Z:%d)\n", //
×
1312
                                        id, board.x, board.y, board.z);
×
1313
                }
×
1314

1315
                void phys(ReportedBoard board) {
1316
                        b.format(
×
1317
                                        "\tBoard for job (%d) board "
1318
                                                        + "[Cabinet:%d,Frame:%d,Board:%d]\n", //
1319
                                        id, board.cabinet, board.frame, board.board);
×
1320
                }
×
1321

1322
                void ip(ReportedBoard board) {
1323
                        b.format("\tBoard for job (%d) board (IP: %s)\n", //
3✔
1324
                                        id, board.address);
3✔
1325
                }
3✔
1326

1327
                void issue(int issueId) {
1328
                        b.format("\t\tAction: noted as issue #%d\n", //
3✔
1329
                                        issueId);
3✔
1330
                }
3✔
1331

1332
                void footer(int numActions) {
1333
                        b.format("\nSummary: %d boards taken out of service.\n",
×
1334
                                        numActions);
×
1335
                }
×
1336

1337
                void serviceActionDone(Reported report) {
1338
                        b.format(
×
1339
                                        "\tAction: board (X:%d,Y:%d,Z:%d) (IP: %s) "
1340
                                                        + "taken out of service once not in use "
1341
                                                        + "(%d problems reported)\n",
1342
                                        report.x, report.y, report.z,
×
1343
                                        report.address, report.numReports);
×
1344
                }
×
1345

1346
                /** @return The assembled message body. */
1347
                @Override
1348
                public String toString() {
1349
                        return b.toString();
×
1350
                }
1351
        }
1352

1353
        private final class JobImpl implements Job {
1354
                @JsonIgnore
1355
                private Epoch epoch;
1356

1357
                private final int id;
1358

1359
                private final int machineId;
1360

1361
                private Integer width;
1362

1363
                private Integer height;
1364

1365
                private Integer depth;
1366

1367
                private JobState state;
1368

1369
                /** If not {@code null}, the ID of the root board of the job. */
1370
                private Integer root;
1371

1372
                private ChipLocation chipRoot;
1373

1374
                private String owner;
1375

1376
                private String keepaliveHost;
1377

1378
                private Instant startTime;
1379

1380
                private Instant keepaliveTime;
1381

1382
                private Instant finishTime;
1383

1384
                private String deathReason;
1385

1386
                private byte[] request;
1387

1388
                private boolean partial;
1389

1390
                private MachineImpl cachedMachine;
1391

1392
                JobImpl(int id, int machineId) {
3✔
1393
                        this.epoch = epochs.getJobsEpoch(id);
3✔
1394
                        this.id = id;
3✔
1395
                        this.machineId = machineId;
3✔
1396
                        partial = true;
3✔
1397
                }
3✔
1398

1399
                JobImpl(int jobId, int machineId, JobState jobState,
1400
                                Instant keepalive) {
1401
                        this(jobId, machineId);
3✔
1402
                        state = jobState;
3✔
1403
                        keepaliveTime = keepalive;
3✔
1404
                }
3✔
1405

1406
                JobImpl(Connection conn, Row row) {
3✔
1407
                        this.id = row.getInt("job_id");
3✔
1408
                        this.machineId = row.getInt("machine_id");
3✔
1409
                        width = row.getInteger("width");
3✔
1410
                        height = row.getInteger("height");
3✔
1411
                        depth = row.getInteger("depth");
3✔
1412
                        root = row.getInteger("root_id");
3✔
1413
                        owner = row.getString("owner");
3✔
1414
                        if (nonNull(root)) {
3✔
1415
                                try (var boardRoot = conn.query(GET_ROOT_OF_BOARD)) {
3✔
1416
                                        chipRoot = boardRoot.call1(chip("root_x", "root_y"), root)
3✔
1417
                                                        .orElse(null);
3✔
1418
                                }
1419
                        }
1420
                        state = row.getEnum("job_state", JobState.class);
3✔
1421
                        keepaliveHost = row.getString("keepalive_host");
3✔
1422
                        keepaliveTime = row.getInstant("keepalive_timestamp");
3✔
1423
                        startTime = row.getInstant("create_timestamp");
3✔
1424
                        finishTime = row.getInstant("death_timestamp");
3✔
1425
                        deathReason = row.getString("death_reason");
3✔
1426
                        request = row.getBytes("original_request");
3✔
1427
                        partial = false;
3✔
1428

1429
                        this.epoch = epochs.getJobsEpoch(id);
3✔
1430
                }
3✔
1431

1432
                /**
1433
                 * Get the machine that this job is running on. May used a cached value.
1434
                 * A transaction is required, but may be a read-only transaction.
1435
                 *
1436
                 * @param conn
1437
                 *            The connection to the DB
1438
                 * @return The overall machine handle.
1439
                 */
1440
                private synchronized MachineImpl getJobMachine(Connection conn) {
1441
                        if (cachedMachine == null || !cachedMachine.epoch.isValid()) {
3✔
1442
                                cachedMachine = Spalloc.this.getMachine(machineId, true, conn)
3✔
1443
                                                .orElseThrow();
3✔
1444
                        }
1445
                        return cachedMachine;
3✔
1446
                }
1447

1448
                @Override
1449
                public void access(String keepaliveAddress) {
1450
                        if (partial) {
3✔
1451
                                throw new PartialJobException();
×
1452
                        }
1453
                        try (var conn = getConnection();
3✔
1454
                                        var keepAlive = conn.update(UPDATE_KEEPALIVE)) {
3✔
1455
                                conn.transaction(() -> keepAlive.call(keepaliveAddress, id));
3✔
1456
                        }
1457
                }
3✔
1458

1459
                @Override
1460
                public void destroy(String reason) {
1461
                        if (partial) {
3✔
1462
                                throw new PartialJobException();
×
1463
                        }
1464
                        powerController.destroyJob(id, reason);
3✔
1465
                        rememberer.closeJob(id);
3✔
1466
                }
3✔
1467

1468
                @Override
1469
                public void setPower(boolean power) {
1470
                        powerController.setPower(id, power ? ON : OFF, READY);
×
1471
                }
×
1472

1473
                @Override
1474
                public boolean waitForChange(Duration timeout) {
1475
                        if (isNull(epoch)) {
3✔
1476
                                return true;
×
1477
                        }
1478
                        try {
1479
                                return epoch.waitForChange(timeout);
×
1480
                        } catch (InterruptedException interrupted) {
3✔
1481
                                currentThread().interrupt();
3✔
1482
                                return false;
3✔
1483
                        }
1484
                }
1485

1486
                @Override
1487
                public int getId() {
1488
                        return id;
3✔
1489
                }
1490

1491
                @Override
1492
                public JobState getState() {
1493
                        return state;
3✔
1494
                }
1495

1496
                @Override
1497
                public Instant getStartTime() {
1498
                        return startTime;
3✔
1499
                }
1500

1501
                @Override
1502
                public Optional<Instant> getFinishTime() {
1503
                        return Optional.ofNullable(finishTime);
3✔
1504
                }
1505

1506
                @Override
1507
                public Optional<String> getReason() {
1508
                        return Optional.ofNullable(deathReason);
3✔
1509
                }
1510

1511
                @Override
1512
                public Optional<String> getKeepaliveHost() {
1513
                        if (partial) {
3✔
1514
                                return Optional.empty();
×
1515
                        }
1516
                        return Optional.ofNullable(keepaliveHost);
3✔
1517
                }
1518

1519
                @Override
1520
                public Instant getKeepaliveTimestamp() {
1521
                        return keepaliveTime;
3✔
1522
                }
1523

1524
                @Override
1525
                public Optional<byte[]> getOriginalRequest() {
1526
                        if (partial) {
3✔
1527
                                return Optional.empty();
×
1528
                        }
1529
                        return Optional.ofNullable(request);
3✔
1530
                }
1531

1532
                @Override
1533
                public Optional<SubMachine> getMachine() {
1534
                        if (isNull(root)) {
3✔
1535
                                return Optional.empty();
3✔
1536
                        }
1537
                        return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
3✔
1538
                }
1539

1540
                @Override
1541
                public Optional<BoardLocation> whereIs(int x, int y) {
1542
                        if (isNull(root)) {
3✔
1543
                                return Optional.empty();
×
1544
                        }
1545
                        try (var conn = getConnection();
3✔
1546
                                        var findBoard = conn.query(findBoardByJobChip)) {
3✔
1547
                                return conn.transaction(false, () -> findBoard
3✔
1548
                                                .call1(row -> new BoardLocationImpl(row,
3✔
1549
                                                                getJobMachine(conn)), id, root, x, y));
3✔
1550
                        }
1551
                }
1552

1553
                // -------------------------------------------------------------
1554
                // Bad board report handling
1555

1556
                @Override
1557
                public String reportIssue(IssueReportRequest report, Permit permit) {
1558
                        try (var q = new BoardReportSQL()) {
3✔
1559
                                var email = new EmailBuilder(id);
3✔
1560
                                var result = q.transaction(
3✔
1561
                                                () -> reportIssue(report, permit, email, q));
3✔
1562
                                emailSender.sendServiceMail(email);
3✔
1563
                                for (var m : report.boards.stream()
3✔
1564
                                                .map(b -> q.getNamedMachine.call1(
3✔
1565
                                                                r -> r.getInt("machine_id"), b.machine, true))
3✔
1566
                                                .collect(toSet())) {
3✔
1567
                                        if (m.isPresent()) {
3✔
1568
                                                epochs.machineChanged(m.get());
×
1569
                                        }
1570
                                }
3✔
1571

1572
                                return result;
3✔
1573
                        } catch (ReportRollbackExn e) {
×
1574
                                return e.getMessage();
×
1575
                        }
1576
                }
1577

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

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

1679
                /**
1680
                 * Record a reported issue with a board.
1681
                 *
1682
                 * @param u
1683
                 *            How to touch the DB
1684
                 * @param boardId
1685
                 *            What board has the issue?
1686
                 * @param issue
1687
                 *            What is the issue?
1688
                 * @param userId
1689
                 *            Who is doing the report?
1690
                 * @param email
1691
                 *            The email we are building.
1692
                 */
1693
                private void addIssueReport(BoardReportSQL u, int boardId, String issue,
1694
                                int userId, EmailBuilder email) {
1695
                        u.insertReport.key(boardId, id, issue, userId)
3✔
1696
                                        .ifPresent(email::issue);
3✔
1697
                }
3✔
1698

1699
                // -------------------------------------------------------------
1700

1701
                @Override
1702
                public Optional<ChipLocation> getRootChip() {
1703
                        return Optional.ofNullable(chipRoot);
3✔
1704
                }
1705

1706
                @Override
1707
                public Optional<String> getOwner() {
1708
                        if (partial) {
3✔
1709
                                return Optional.empty();
×
1710
                        }
1711
                        return Optional.ofNullable(owner);
3✔
1712
                }
1713

1714
                @Override
1715
                public Optional<Integer> getWidth() {
1716
                        return Optional.ofNullable(width);
3✔
1717
                }
1718

1719
                @Override
1720
                public Optional<Integer> getHeight() {
1721
                        return Optional.ofNullable(height);
3✔
1722
                }
1723

1724
                @Override
1725
                public Optional<Integer> getDepth() {
1726
                        return Optional.ofNullable(depth);
3✔
1727
                }
1728

1729
                @Override
1730
                public void rememberProxy(ProxyCore proxy) {
1731
                        rememberer.rememberProxyForJob(id, proxy);
×
1732
                }
×
1733

1734
                @Override
1735
                public void forgetProxy(ProxyCore proxy) {
1736
                        rememberer.removeProxyForJob(id, proxy);
×
1737
                }
×
1738

1739
                @Override
1740
                @SuppressWarnings("MustBeClosed")
1741
                public TransceiverInterface getTransceiver() throws IOException,
1742
                                InterruptedException, SpinnmanException {
1743
                        var mac = getMachine();
×
1744
                        if (mac.isEmpty()) {
×
1745
                                throw new IllegalStateException(
×
1746
                                                "Job is not active!");
1747
                        }
1748
                        var txrx = rememberer.getTransceiverForJob(id);
×
1749
                        if (nonNull(txrx)) {
×
1750
                                return txrx;
×
1751
                        }
1752
                        List<uk.ac.manchester.spinnaker.connections.model.Connection>
1753
                                connections = new ArrayList<>();
×
1754
                        for (var conn : mac.get().getConnections()) {
×
1755
                                connections.add(new SCPConnection(conn.getChip(),
×
1756
                                                null, null, InetAddress.getByName(conn.getHostname())));
×
1757
                        }
×
1758
                        txrx = new Transceiver(MachineVersion.FIVE, connections);
×
1759
                        var unused = txrx.getMachineDetails();
×
1760
                        rememberer.rememberTransceiverForJob(id, txrx);
×
1761
                        return txrx;
×
1762
                }
1763

1764
                @Override
1765
                @SuppressWarnings("MustBeClosed")
1766
                public FastDataIn getFastDataIn(CoreLocation gathererCore, IPTag iptag)
1767
                                throws ProcessException, IOException, InterruptedException {
1768
                        var fdi = rememberer.getFastDataIn(id, iptag.getDestination());
×
1769
                        if (fdi != null) {
×
1770
                                return fdi;
×
1771
                        }
1772
                        fdi = new FastDataIn(gathererCore, iptag);
×
1773
                        rememberer.rememberFastDataIn(id, iptag.getDestination(), fdi);
×
1774
                        return fdi;
×
1775
                }
1776

1777
                @Override
1778
                @SuppressWarnings("MustBeClosed")
1779
                public Downloader getDownloader(IPTag iptag)
1780
                                throws ProcessException, IOException, InterruptedException {
1781
                        var downloader = rememberer.getDownloader(id,
×
1782
                                        iptag.getDestination());
×
1783
                        if (downloader != null) {
×
1784
                                // Ensure the downloader can be reuse
1785
                                downloader.reuse();
×
1786
                                return downloader;
×
1787
                        }
1788
                        downloader = new Downloader(iptag);
×
1789
                        rememberer.rememberDownloader(id, iptag.getDestination(),
×
1790
                                        downloader);
1791
                        return downloader;
×
1792
                }
1793

1794
                @Override
1795
                public boolean equals(Object other) {
1796
                        // Equality is defined exactly by the database ID
1797
                        return (other instanceof JobImpl) && (id == ((JobImpl) other).id);
×
1798
                }
1799

1800
                @Override
1801
                public int hashCode() {
1802
                        return id;
×
1803
                }
1804

1805
                @Override
1806
                public String toString() {
1807
                        return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1808
                                        width, height, depth, startTime, finishTime);
1809
                }
1810

1811
                private final class SubMachineImpl implements SubMachine {
1812
                        /** The machine that this sub-machine is part of. */
1813
                        private final Machine machine;
1814

1815
                        /** The root X coordinate of this sub-machine. */
1816
                        private int rootX;
1817

1818
                        /** The root Y coordinate of this sub-machine. */
1819
                        private int rootY;
1820

1821
                        /** The root Z coordinate of this sub-machine. */
1822
                        private int rootZ;
1823

1824
                        /** The connection details of this sub-machine. */
1825
                        private List<ConnectionInfo> connections;
1826

1827
                        /** The board locations of this sub-machine. */
1828
                        private List<BoardCoordinates> boards;
1829

1830
                        private List<Integer> boardIds;
1831

1832
                        private SubMachineImpl(Connection conn) {
3✔
1833
                                machine = getJobMachine(conn);
3✔
1834
                                try (var getRootXY = conn.query(GET_ROOT_COORDS);
3✔
1835
                                                var getBoardInfo = conn.query(GET_BOARD_CONNECT_INFO)) {
3✔
1836
                                        getRootXY.call1(row -> {
3✔
1837
                                                rootX = row.getInt("x");
3✔
1838
                                                rootY = row.getInt("y");
3✔
1839
                                                rootZ = row.getInt("z");
3✔
1840
                                                // We have to return something,
1841
                                                // but it doesn't matter what
1842
                                                return true;
3✔
1843
                                        }, root);
1844
                                        int capacityEstimate = width * height;
3✔
1845
                                        connections = new ArrayList<>(capacityEstimate);
3✔
1846
                                        boards = new ArrayList<>(capacityEstimate);
3✔
1847
                                        boardIds = new ArrayList<>(capacityEstimate);
3✔
1848
                                        getBoardInfo.call(row -> {
3✔
1849
                                                boardIds.add(row.getInt("board_id"));
3✔
1850
                                                boards.add(new BoardCoordinates(row.getInt("x"),
3✔
1851
                                                                row.getInt("y"), row.getInt("z")));
3✔
1852
                                                connections.add(new ConnectionInfo(
3✔
1853
                                                                relativeChipLocation(row.getInt("root_x"),
3✔
1854
                                                                                row.getInt("root_y")),
3✔
1855
                                                                row.getString("address")));
3✔
1856
                                                // We have to return something,
1857
                                                // but it doesn't matter what
1858
                                                return true;
3✔
1859
                                        }, id);
3✔
1860
                                }
1861
                        }
3✔
1862

1863
                        private ChipLocation relativeChipLocation(int x, int y) {
1864
                                x -= chipRoot.getX();
3✔
1865
                                y -= chipRoot.getY();
3✔
1866
                                // Allow for wrapping
1867
                                if (x < 0) {
3✔
1868
                                        x += machine.getWidth() * TRIAD_CHIP_SIZE;
×
1869
                                }
1870
                                if (y < 0) {
3✔
1871
                                        y += machine.getHeight() * TRIAD_CHIP_SIZE;
×
1872
                                }
1873
                                return new ChipLocation(x, y);
3✔
1874
                        }
1875

1876
                        @Override
1877
                        public Machine getMachine() {
1878
                                return machine;
3✔
1879
                        }
1880

1881
                        @Override
1882
                        public int getRootX() {
1883
                                return rootX;
3✔
1884
                        }
1885

1886
                        @Override
1887
                        public int getRootY() {
1888
                                return rootY;
3✔
1889
                        }
1890

1891
                        @Override
1892
                        public int getRootZ() {
1893
                                return rootZ;
3✔
1894
                        }
1895

1896
                        @Override
1897
                        public int getWidth() {
1898
                                return width;
3✔
1899
                        }
1900

1901
                        @Override
1902
                        public int getHeight() {
1903
                                return height;
3✔
1904
                        }
1905

1906
                        @Override
1907
                        public int getDepth() {
1908
                                return depth;
3✔
1909
                        }
1910

1911
                        @Override
1912
                        public List<ConnectionInfo> getConnections() {
1913
                                return connections;
3✔
1914
                        }
1915

1916
                        @Override
1917
                        public List<BoardCoordinates> getBoards() {
1918
                                return boards;
3✔
1919
                        }
1920

1921
                        @Override
1922
                        public PowerState getPower() {
1923
                                try (var conn = getConnection();
3✔
1924
                                                var power = conn.query(GET_SUM_BOARDS_POWERED)) {
3✔
1925
                                        return conn.transaction(false,
3✔
1926
                                                        () -> power.call1(integer("total_on"), id)
3✔
1927
                                                                        .map(totalOn -> totalOn < boardIds.size()
3✔
1928
                                                                                        ? OFF
3✔
1929
                                                                                        : ON)
×
1930
                                                                        .orElse(null));
3✔
1931
                                }
1932
                        }
1933

1934
                        @Override
1935
                        public void setPower(PowerState ps) {
1936
                                if (partial) {
3✔
1937
                                        throw new PartialJobException();
×
1938
                                }
1939
                                powerController.setPower(id, ps, READY);
3✔
1940
                        }
3✔
1941
                }
1942
        }
1943

1944
        /**
1945
         * Board location implementation. Does not retain database connections after
1946
         * creation.
1947
         *
1948
         * @author Donal Fellows
1949
         */
1950
        private final class BoardLocationImpl implements BoardLocation {
1951
                private JobImpl job;
1952

1953
                private final String machineName;
1954

1955
                private final int machineWidth;
1956

1957
                private final int machineHeight;
1958

1959
                private final ChipLocation chip;
1960

1961
                private final ChipLocation boardChip;
1962

1963
                private final BoardCoordinates logical;
1964

1965
                private final BoardPhysicalCoordinates physical;
1966

1967
                // Transaction is open
1968
                private BoardLocationImpl(Row row, Machine machine) {
3✔
1969
                        machineName = row.getString("machine_name");
3✔
1970
                        logical = new BoardCoordinates(row.getInt("x"), row.getInt("y"),
3✔
1971
                                        row.getInt("z"));
3✔
1972
                        physical = new BoardPhysicalCoordinates(row.getInt("cabinet"),
3✔
1973
                                        row.getInt("frame"), row.getInteger("board_num"));
3✔
1974
                        chip = row.getChip("chip_x", "chip_y");
3✔
1975
                        machineWidth = machine.getWidth();
3✔
1976
                        machineHeight = machine.getHeight();
3✔
1977
                        var boardX = row.getInteger("board_chip_x");
3✔
1978
                        if (nonNull(boardX)) {
3✔
1979
                                boardChip = row.getChip("board_chip_x", "board_chip_y");
3✔
1980
                        } else {
1981
                                boardChip = chip;
×
1982
                        }
1983

1984
                        var jobId = row.getInteger("job_id");
3✔
1985
                        if (nonNull(jobId)) {
3✔
1986
                                job = new JobImpl(jobId, machine.getId());
3✔
1987
                                job.chipRoot = row.getChip("job_root_chip_x",
3✔
1988
                                                "job_root_chip_y");
1989
                        }
1990
                }
3✔
1991

1992
                @Override
1993
                public ChipLocation getBoardChip() {
1994
                        return boardChip;
3✔
1995
                }
1996

1997
                @Override
1998
                public ChipLocation getChipRelativeTo(ChipLocation rootChip) {
1999
                        int x = chip.getX() - rootChip.getX();
3✔
2000
                        if (x < 0) {
3✔
2001
                                x += machineWidth * TRIAD_CHIP_SIZE;
×
2002
                        }
2003
                        int y = chip.getY() - rootChip.getY();
3✔
2004
                        if (y < 0) {
3✔
2005
                                y += machineHeight * TRIAD_CHIP_SIZE;
×
2006
                        }
2007
                        return new ChipLocation(x, y);
3✔
2008
                }
2009

2010
                @Override
2011
                public String getMachine() {
2012
                        return machineName;
3✔
2013
                }
2014

2015
                @Override
2016
                public BoardCoordinates getLogical() {
2017
                        return logical;
3✔
2018
                }
2019

2020
                @Override
2021
                public BoardPhysicalCoordinates getPhysical() {
2022
                        return physical;
3✔
2023
                }
2024

2025
                @Override
2026
                public ChipLocation getChip() {
2027
                        return chip;
3✔
2028
                }
2029

2030
                @Override
2031
                public Job getJob() {
2032
                        return job;
3✔
2033
                }
2034
        }
2035

2036
        static class PartialJobException extends IllegalStateException {
2037
                private static final long serialVersionUID = 2997856394666135483L;
2038

2039
                PartialJobException() {
2040
                        super("partial job only");
×
2041
                }
×
2042
        }
2043
}
2044

2045
class ReportRollbackExn extends RuntimeException {
2046
        private static final long serialVersionUID = 1L;
2047

2048
        @FormatMethod
2049
        ReportRollbackExn(String msg, Object... args) {
2050
                super(format(msg, args));
×
2051
        }
×
2052

2053
        ReportRollbackExn(HasChipLocation chip) {
2054
                this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
2055
                                chip.getY());
×
2056
        }
×
2057
}
2058

2059
abstract class GroupsException extends RuntimeException {
2060
        private static final long serialVersionUID = 6607077117924279611L;
2061

2062
        GroupsException(String message) {
2063
                super(message);
×
2064
        }
×
2065

2066
        GroupsException(String message, Throwable cause) {
2067
                super(message, cause);
×
2068
        }
×
2069
}
2070

2071
class NoSuchGroupException extends GroupsException {
2072
        private static final long serialVersionUID = 5193818294198205503L;
2073

2074
        @FormatMethod
2075
        NoSuchGroupException(String msg, Object... args) {
2076
                super(format(msg, args));
×
2077
        }
×
2078
}
2079

2080
class MultipleGroupsException extends GroupsException {
2081
        private static final long serialVersionUID = 6284332340565334236L;
2082

2083
        @FormatMethod
2084
        MultipleGroupsException(String msg, Object... args) {
2085
                super(format(msg, args));
×
2086
        }
×
2087
}
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