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

SpiNNakerManchester / JavaSpiNNaker / 13502930948

24 Feb 2025 04:31PM UTC coverage: 38.515% (-0.01%) from 38.525%
13502930948

push

github

rowleya
Client and server side of Fast Data Write

653 of 981 new or added lines in 8 files covered. (66.56%)

4 existing lines in 2 files now uncovered.

9190 of 23861 relevant lines covered (38.51%)

1.15 hits per line

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

74.44
/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.machine.ChipLocation;
86
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
87
import uk.ac.manchester.spinnaker.machine.HasCoreLocation;
88
import uk.ac.manchester.spinnaker.machine.MachineVersion;
89
import uk.ac.manchester.spinnaker.machine.board.BMPCoords;
90
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
91
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
92
import uk.ac.manchester.spinnaker.spalloc.messages.BoardCoordinates;
93
import uk.ac.manchester.spinnaker.spalloc.messages.BoardPhysicalCoordinates;
94
import uk.ac.manchester.spinnaker.transceiver.SpinnmanException;
95
import uk.ac.manchester.spinnaker.transceiver.Transceiver;
96
import uk.ac.manchester.spinnaker.transceiver.TransceiverInterface;
97

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

109
    private static final Logger log = getLogger(Spalloc.class);
3✔
110

111
    @Autowired
112
    private PowerController powerController;
113

114
    @Autowired
115
    private Epochs epochs;
116

117
    @Autowired
118
    private QuotaManager quotaManager;
119

120
    @Autowired
121
    private ReportMailSender emailSender;
122

123
    @Autowired
124
    private AllocatorProperties props;
125

126
    @Autowired
127
    private JobObjectRememberer rememberer;
128

129
    @Autowired
130
    private AllocatorTask allocator;
131

132
    @GuardedBy("this")
3✔
133
    private transient Map<String, List<BoardCoords>> downBoardsCache =
134
            new HashMap<>();
135

136
    @GuardedBy("this")
3✔
137
    private transient Map<String, List<DownLink>> downLinksCache =
138
            new HashMap<>();
139

140
    @Override
141
    public Map<String, Machine> getMachines(boolean allowOutOfService) {
142
        return executeRead(c -> getMachines(c, allowOutOfService));
3✔
143
    }
144

145
    private Map<String, Machine> getMachines(Connection conn,
146
            boolean allowOutOfService) {
147
        try (var listMachines = conn.query(GET_ALL_MACHINES)) {
3✔
148
            return Row.stream(listMachines.call(
3✔
149
                    row -> new MachineImpl(conn, row), allowOutOfService))
3✔
150
                    .toMap(Machine::getName, (m) -> m);
3✔
151
        }
152
    }
153

154
    private final class ListMachinesSQL extends AbstractSQL {
3✔
155
        private final Query listMachines = conn.query(GET_ALL_MACHINES);
3✔
156

157
        private final Query countMachineThings =
3✔
158
                conn.query(COUNT_MACHINE_THINGS);
3✔
159

160
        private final Query getTags = conn.query(GET_TAGS);
3✔
161

162
        @Override
163
        public void close() {
164
            listMachines.close();
3✔
165
            countMachineThings.close();
3✔
166
            getTags.close();
3✔
167
            super.close();
3✔
168
        }
3✔
169

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

189
    @Override
190
    public List<MachineListEntryRecord>
191
            listMachines(boolean allowOutOfService) {
192
        try (var sql = new ListMachinesSQL()) {
3✔
193
            return sql.transactionRead(
3✔
194
                    () -> sql.listMachines.call(
3✔
195
                            sql::makeMachineListEntryRecord,
3✔
196
                            allowOutOfService));
3✔
197
        }
198
    }
199

200
    @Override
201
    public Optional<Machine> getMachine(String name,
202
            boolean allowOutOfService) {
203
        return executeRead(
3✔
204
                conn -> getMachine(name, allowOutOfService, conn).map(m -> m));
3✔
205
    }
206

207
    private Optional<MachineImpl> getMachine(int id, boolean allowOutOfService,
208
            Connection conn) {
209
        try (var idMachine = conn.query(GET_MACHINE_BY_ID)) {
3✔
210
            return idMachine.call1(row -> new MachineImpl(conn, row),
3✔
211
                    id, allowOutOfService);
3✔
212
        }
213
    }
214

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

223
    private final class DescribeMachineSQL extends AbstractSQL {
3✔
224
        final Query namedMachine = conn.query(GET_NAMED_MACHINE);
3✔
225

226
        final Query countMachineThings = conn.query(COUNT_MACHINE_THINGS);
3✔
227

228
        final Query getTags = conn.query(GET_TAGS);
3✔
229

230
        final Query getJobs = conn.query(GET_MACHINE_JOBS);
3✔
231

232
        final Query getCoords = conn.query(GET_JOB_BOARD_COORDS);
3✔
233

234
        final Query getLive = conn.query(GET_LIVE_BOARDS);
3✔
235

236
        final Query getDead = conn.query(GET_DEAD_BOARDS);
3✔
237

238
        final Query getQuota = conn.query(GET_USER_QUOTA);
3✔
239

240
        @Override
241
        public void close() {
242
            namedMachine.close();
3✔
243
            countMachineThings.close();
3✔
244
            getTags.close();
3✔
245
            getJobs.close();
3✔
246
            getCoords.close();
3✔
247
            getLive.close();
3✔
248
            getDead.close();
3✔
249
            getQuota.close();
3✔
250
            super.close();
3✔
251
        }
3✔
252
    }
253

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

280
    private static MachineDescription getBasicMachineInfo(Row row) {
281
        var md = new MachineDescription();
3✔
282
        md.setId(row.getInt("machine_id"));
3✔
283
        md.setName(row.getString("machine_name"));
3✔
284
        md.setWidth(row.getInt("width"));
3✔
285
        md.setHeight(row.getInt("height"));
3✔
286
        return md;
3✔
287
    }
288

289
    private static JobInfo getMachineJobInfo(Permit permit, Query getCoords,
290
            Row row) {
291
        int jobId = row.getInt("job_id");
3✔
292
        var mayUnveil = permit.unveilFor(row.getString("owner_name"));
3✔
293
        var owner = mayUnveil ? row.getString("owner_name") : null;
3✔
294

295
        var ji = new JobInfo();
3✔
296
        ji.setId(jobId);
3✔
297
        ji.setOwner(owner);
3✔
298
        ji.setBoards(
3✔
299
                getCoords.call(r -> new BoardCoords(r, !mayUnveil), jobId));
3✔
300
        return ji;
3✔
301
    }
302

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

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

335
    @Override
336
    public List<JobListEntryRecord> listJobs(Permit permit) {
337
        return executeRead(conn -> {
3✔
338
            try (var listLiveJobs = conn.query(LIST_LIVE_JOBS);
3✔
339
                    var countPoweredBoards = conn.query(COUNT_POWERED_BOARDS);
3✔
340
                    var getCoords = conn.query(GET_JOB_BOARD_COORDS)) {
3✔
341
                return listLiveJobs.call(row -> makeJobListEntryRecord(permit,
3✔
342
                        countPoweredBoards, getCoords, row));
343
            }
344
        });
345
    }
346

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

370
    @Override
371
    @PostFilter(MAY_SEE_JOB_DETAILS)
372
    public Optional<Job> getJob(Permit permit, int id) {
373
        return executeRead(conn -> getJob(id, conn).map(j -> (Job) j));
3✔
374
    }
375

376
    private Optional<JobImpl> getJob(int id, Connection conn) {
377
        try (var s = conn.query(GET_JOB)) {
3✔
378
            return s.call1(row -> new JobImpl(conn, row), id);
3✔
379
        }
380
    }
381

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

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

424
    @Override
425
    public Job createJobInGroup(String owner, String groupName,
426
            CreateDescriptor descriptor, String machineName, List<String> tags,
427
            Duration keepaliveInterval, byte[] req)
428
                    throws IllegalArgumentException {
429
        return execute(conn -> {
3✔
430
            int user = getUser(conn, owner).orElseThrow(
3✔
NEW
431
                    () -> new RuntimeException("no such user: " + owner));
×
432
            int group = selectGroup(conn, owner, groupName);
3✔
433
            if (!quotaManager.mayCreateJob(group)) {
3✔
434
                // No quota left
NEW
435
                throw new IllegalArgumentException(
×
436
                        "quota exceeded in group " + group);
437
            }
438

439
            var m = selectMachine(conn, descriptor, machineName, tags);
3✔
440
            if (!m.isPresent()) {
3✔
NEW
441
                throw new IllegalArgumentException(
×
442
                        "no machine available which matches allocation "
443
                        + "request");
444
            }
445
            var machine = m.orElseThrow();
3✔
446

447
            var id = insertJob(conn, machine, user, group, keepaliveInterval,
3✔
448
                    req);
449
            if (!id.isPresent()) {
3✔
NEW
450
                throw new RuntimeException("failed to create job");
×
451
            }
452
            int jobId = id.orElseThrow();
3✔
453

454
            var scale = props.getPriorityScale();
3✔
455

456
            if (machine.getArea() < descriptor.getArea()) {
3✔
NEW
457
                throw new IllegalArgumentException(
×
458
                        "request cannot fit on machine");
459
            }
460

461
            // Ask the allocator engine to do the allocation
462
            int numBoards = descriptor.visit(new CreateVisitor<Integer>() {
3✔
463
                @Override
464
                public Integer numBoards(CreateNumBoards nb) {
465
                    try (var insertReq = conn.update(INSERT_REQ_N_BOARDS)) {
3✔
466
                        insertReq.call(jobId, nb.numBoards, nb.maxDead,
3✔
467
                                (int) (nb.getArea() * scale.getSize()));
3✔
468
                    }
469
                    return nb.numBoards;
3✔
470
                }
471

472
                @Override
473
                public Integer dimensions(CreateDimensions d) {
474
                    try (var insertReq = conn.update(INSERT_REQ_SIZE)) {
3✔
475
                        insertReq.call(jobId, d.width, d.height, d.maxDead,
3✔
476
                                (int) (d.getArea() * scale.getDimensions()));
3✔
477
                    }
478
                    return max(1, d.getArea() - d.maxDead);
3✔
479
                }
480

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

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

517
            // DB now changed; can report success
518
            JobLifecycle.log.info(
3✔
519
                    "created job {} on {} for {} asking for {} board(s)", jobId,
3✔
520
                    machine.name, owner, numBoards);
3✔
521

522
            allocator.scheduleAllocateNow();
3✔
523
            return getJob(jobId, conn).map(ji -> (Job) ji).orElseThrow(
3✔
NEW
524
                    () -> new RuntimeException("Error creating job!"));
×
525
        });
526
    }
527

528
    @Override
529
    public Job createJob(String owner, CreateDescriptor descriptor,
530
            String machineName, List<String> tags, Duration keepaliveInterval,
531
            byte[] originalRequest) {
532
        return execute(conn -> createJobInGroup(
3✔
533
                owner, getOnlyGroup(conn, owner), descriptor, machineName,
3✔
534
                tags, keepaliveInterval, originalRequest));
535
    }
536

537
    @Override
538
    public Job createJobInCollabSession(String owner,
539
            String nmpiCollab, CreateDescriptor descriptor,
540
            String machineName, List<String> tags, Duration keepaliveInterval,
541
            byte[] originalRequest) {
NEW
542
        var session = quotaManager.createSession(nmpiCollab, owner);
×
NEW
543
        var quotaUnits = session.getResourceUsage().getUnits();
×
544

545
        // Use the Collab name as the group, as it should exist
NEW
546
        var job = execute(conn -> createJobInGroup(
×
547
                owner, nmpiCollab, descriptor, machineName,
548
                tags, keepaliveInterval, originalRequest));
549

NEW
550
        quotaManager.associateNMPISession(job.getId(), session.getId(),
×
551
                quotaUnits);
552

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

557
    @Override
558
    public Job createJobForNMPIJob(String owner, int nmpiJobId,
559
            CreateDescriptor descriptor, String machineName, List<String> tags,
560
            Duration keepaliveInterval,        byte[] originalRequest) {
NEW
561
        var collab = quotaManager.mayUseNMPIJob(owner, nmpiJobId);
×
NEW
562
        if (collab.isEmpty()) {
×
NEW
563
            throw new IllegalArgumentException("User cannot create session in "
×
564
                    + "NMPI job" + nmpiJobId);
565
        }
NEW
566
        var quotaDetails = collab.get();
×
567

NEW
568
        var job = execute(conn -> createJobInGroup(
×
569
                owner, quotaDetails.collabId, descriptor, machineName,
570
                tags, keepaliveInterval, originalRequest));
571

NEW
572
        quotaManager.associateNMPIJob(job.getId(), nmpiJobId,
×
573
                quotaDetails.quotaUnits);
574

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

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

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

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

627
    private class BoardLocated {
628
        int boardId;
629

630
        int z;
631

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

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

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

691
    private Optional<MachineImpl> selectMachine(Connection conn,
692
            CreateDescriptor descriptor, String machineName,
693
            List<String> tags) {
694
        if (nonNull(machineName)) {
3✔
695
            var m = getMachine(machineName, false, conn);
3✔
696
            if (m.isPresent() && isAllocPossible(conn, descriptor, m.get())) {
3✔
697
                return m;
3✔
698
            }
NEW
699
            return Optional.empty();
×
700
        }
701

NEW
702
        if (!tags.isEmpty()) {
×
NEW
703
            for (var m : getMachines(conn, false).values()) {
×
NEW
704
                var mi = (MachineImpl) m;
×
NEW
705
                if (mi.tags.containsAll(tags)
×
NEW
706
                        && isAllocPossible(conn, descriptor, mi)) {
×
NEW
707
                    return Optional.of(mi);
×
708
                }
NEW
709
            }
×
710
        }
NEW
711
        return Optional.empty();
×
712
    }
713

714
    private boolean isAllocPossible(final Connection conn,
715
            final CreateDescriptor descriptor,
716
            final MachineImpl m) {
717
        return descriptor.visit(new CreateVisitor<Boolean>() {
3✔
718
            @Override
719
            public Boolean numBoards(CreateNumBoards nb) {
720
                try (var getNBoards = conn.query(COUNT_FUNCTIONING_BOARDS)) {
3✔
721
                    var numBoards = getNBoards.call1(integer("c"), m.id)
3✔
722
                            .orElseThrow();
3✔
723
                    return numBoards >= nb.numBoards;
3✔
724
                }
725
            }
726

727
            @Override
728
            public Boolean dimensions(CreateDimensions d) {
729
                try (var checkPossible = conn.query(checkRectangle)) {
3✔
730
                    return checkPossible.call1((r) -> true, d.width, d.height,
3✔
731
                            m.id, d.maxDead).isPresent();
3✔
732
                }
733
            }
734

735
            @Override
736
            public Boolean dimensionsAt(CreateDimensionsAt da) {
737
                try (var checkPossible = conn.query(checkRectangleAt)) {
3✔
738
                    int board = locateBoard(conn, m.name, da, true);
3✔
739
                    return checkPossible.call1((r) -> true, board,
3✔
740
                            da.width, da.height, m.id, da.maxDead).isPresent();
3✔
NEW
741
                } catch (IllegalArgumentException e) {
×
742
                    // This means the board doesn't exist on the given machine
NEW
743
                    return false;
×
744
                }
745
            }
746

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

760
    @Override
761
    public void purgeDownCache() {
NEW
762
        synchronized (this) {
×
NEW
763
            downBoardsCache.clear();
×
NEW
764
            downLinksCache.clear();
×
NEW
765
        }
×
NEW
766
    }
×
767

768
    private static String mergeDescription(HasChipLocation coreLocation,
769
            String description) {
770
        if (isNull(description)) {
3✔
NEW
771
            description = "<null>";
×
772
        }
773
        if (coreLocation instanceof HasCoreLocation) {
3✔
NEW
774
            var loc = (HasCoreLocation) coreLocation;
×
NEW
775
            description += format(" (at core %d of chip %s)", loc.getP(),
×
NEW
776
                    loc.asChipLocation());
×
777
        } else if (nonNull(coreLocation)) {
3✔
NEW
778
            description +=
×
NEW
779
                    format(" (at chip %s)", coreLocation.asChipLocation());
×
780
        }
781
        return description;
3✔
782
    }
783

784
    private class Problem {
785
        int boardId;
786

787
        Integer jobId;
788

789
        Problem(Row row) {
3✔
790
            boardId = row.getInt("board_id");
3✔
791
            jobId = row.getInt("job_id");
3✔
792
        }
3✔
793
    }
794

795
    @Override
796
    public void reportProblem(String address, HasChipLocation coreLocation,
797
            String description, Permit permit) {
798
        try (var sql = new BoardReportSQL()) {
3✔
799
            var desc = mergeDescription(coreLocation, description);
3✔
800
            var email = sql.transaction(() -> {
3✔
801
                var machines = getMachines(sql.getConnection(), true).values();
3✔
802
                for (var m : machines) {
3✔
803
                    var mail = sql.findBoardNet.call1(
3✔
804
                            Problem::new, m.getId(), address)
3✔
805
                            .flatMap(prob -> reportProblem(prob, desc, permit,
3✔
806
                                    sql));
807
                    if (mail.isPresent()) {
3✔
NEW
808
                        return mail;
×
809
                    }
810
                }
3✔
811
                return Optional.empty();
3✔
812
            });
813
            // Outside the transaction!
814
            email.ifPresent(emailSender::sendServiceMail);
3✔
NEW
815
        } catch (ReportRollbackExn e) {
×
NEW
816
            log.warn("failed to handle problem report", e);
×
817
        }
3✔
818
    }
3✔
819

820
    private Optional<EmailBuilder> reportProblem(Problem problem,
821
            String description,        Permit permit, BoardReportSQL sql) {
822
        var email = new EmailBuilder(problem.jobId);
3✔
823
        email.header(description, 1, permit.name);
3✔
824
        int userId = getUser(sql.getConnection(), permit.name).orElseThrow(
3✔
NEW
825
                () -> new ReportRollbackExn("no such user: %s", permit.name));
×
826
        sql.insertReport.key(problem.boardId, problem.jobId,
3✔
827
                description, userId).ifPresent(email::issue);
3✔
828
        return takeBoardsOutOfService(sql, email).map(acted -> {
3✔
NEW
829
            email.footer(acted);
×
NEW
830
            return email;
×
831
        });
832
    }
833

834
    private class Reported {
835
        int boardId;
836

837
        int x;
838

839
        int y;
840

841
        int z;
842

843
        String address;
844

845
        int numReports;
846

NEW
847
        Reported(Row row) {
×
NEW
848
            boardId = row.getInt("board_id");
×
NEW
849
            x = row.getInt("x");
×
NEW
850
            y = row.getInt("y");
×
NEW
851
            z = row.getInt("z");
×
NEW
852
            address = row.getString("address");
×
NEW
853
            numReports = row.getInt("numReports");
×
NEW
854
        }
×
855

856
    }
857

858
    /**
859
     * Take boards out of service if they've been reported frequently enough.
860
     *
861
     * @param sql
862
     *            How to touch the DB
863
     * @param email
864
     *            The email we are building.
865
     * @return The number of boards taken out of service
866
     */
867
    private Optional<Integer> takeBoardsOutOfService(BoardReportSQL sql,
868
            EmailBuilder email) {
869
        int acted = 0;
3✔
870
        for (var report : sql.getReported.call(Reported::new,
3✔
871
                props.getReportActionThreshold())) {
3✔
NEW
872
            if (sql.setFunctioning.call(false, report.boardId) > 0) {
×
NEW
873
                email.serviceActionDone(report);
×
NEW
874
                acted++;
×
875
            }
NEW
876
        }
×
877
        if (acted > 0) {
3✔
NEW
878
            purgeDownCache();
×
879
        }
880
        return acted > 0 ? Optional.of(acted) : Optional.empty();
3✔
881
    }
882

883
    private static DownLink makeDownLinkFromRow(Row row) {
884
        // Non-standard column names to reduce number of queries
NEW
885
        var board1 = new BoardCoords(row.getInt("board_1_x"),
×
NEW
886
                row.getInt("board_1_y"), row.getInt("board_1_z"),
×
NEW
887
                row.getInt("board_1_c"), row.getInt("board_1_f"),
×
NEW
888
                row.getInteger("board_1_b"), row.getString("board_1_addr"));
×
NEW
889
        var board2 = new BoardCoords(row.getInt("board_2_x"),
×
NEW
890
                row.getInt("board_2_y"), row.getInt("board_2_z"),
×
NEW
891
                row.getInt("board_2_c"), row.getInt("board_2_f"),
×
NEW
892
                row.getInteger("board_2_b"), row.getString("board_2_addr"));
×
NEW
893
        return new DownLink(board1, row.getEnum("dir_1", Direction.class),
×
NEW
894
                board2, row.getEnum("dir_2", Direction.class));
×
895
    }
896

897
    private class MachineImpl implements Machine {
898
        private final int id;
899

900
        private final boolean inService;
901

902
        private final String name;
903

904
        private final Set<String> tags;
905

906
        private final int width;
907

908
        private final int height;
909

910
        private boolean lookedUpWraps;
911

912
        private boolean hWrap;
913

914
        private boolean vWrap;
915

916
        @JsonIgnore
917
        private final Epoch epoch;
918

919
        MachineImpl(Connection conn, Row rs) {
3✔
920
            id = rs.getInt("machine_id");
3✔
921
            name = rs.getString("machine_name");
3✔
922
            width = rs.getInt("width");
3✔
923
            height = rs.getInt("height");
3✔
924
            inService = rs.getBoolean("in_service");
3✔
925
            lookedUpWraps = false;
3✔
926
            try (var getTags = conn.query(GET_TAGS)) {
3✔
927
                tags = Row.stream(copy(getTags.call(string("tag"), id)))
3✔
928
                        .toSet();
3✔
929
            }
930

931
            this.epoch = epochs.getMachineEpoch(id);
3✔
932
        }
3✔
933

934
        private int getArea() {
935
            return width * height * TRIAD_DEPTH;
3✔
936
        }
937

938
        @Override
939
        public boolean waitForChange(Duration timeout) {
940
            if (isNull(epoch)) {
3✔
NEW
941
                log.info("Machine {} epoch is null!", id);
×
NEW
942
                return true;
×
943
            }
944
            try {
945
                log.info("Waiting for change in epoch for {}", id);
3✔
NEW
946
                return epoch.waitForChange(timeout);
×
947
            } catch (InterruptedException interrupted) {
3✔
948
                log.info("Interrupted waiting for change on {}", id);
3✔
949
                return false;
3✔
950
            }
951
        }
952

953
        @Override
954
        public Optional<BoardLocation> getBoardByChip(HasChipLocation chip) {
955
            try (var conn = getConnection();
3✔
956
                    var findBoard = conn.query(findBoardByGlobalChip)) {
3✔
957
                return conn.transaction(false,
3✔
958
                        () -> findBoard.call1(
3✔
959
                                row -> new BoardLocationImpl(row, this), id,
3✔
960
                                chip.getX(), chip.getY()));
3✔
961
            }
962
        }
963

964
        @Override
965
        public Optional<BoardLocation> getBoardByPhysicalCoords(
966
                PhysicalCoords coords) {
967
            try (var conn = getConnection();
3✔
968
                    var findBoard = conn.query(findBoardByPhysicalCoords)) {
3✔
969
                return conn.transaction(false,
3✔
970
                        () -> findBoard.call1(
3✔
971
                                row -> new BoardLocationImpl(row, this), id,
3✔
972
                                coords.c, coords.f, coords.b));
3✔
973
            }
974
        }
975

976
        @Override
977
        public Optional<BoardLocation> getBoardByLogicalCoords(
978
                TriadCoords coords) {
979
            try (var conn = getConnection();
3✔
980
                    var findBoard = conn.query(findBoardByLogicalCoords)) {
3✔
981
                return conn.transaction(false,
3✔
982
                        () -> findBoard.call1(
3✔
983
                                row -> new BoardLocationImpl(row, this), id,
3✔
984
                                coords.x, coords.y, coords.z));
3✔
985
            }
986
        }
987

988
        @Override
989
        public Optional<BoardLocation> getBoardByIPAddress(String address) {
990
            try (var conn = getConnection();
3✔
991
                    var findBoard = conn.query(findBoardByIPAddress)) {
3✔
992
                return conn.transaction(false,
3✔
993
                        () -> findBoard.call1(
3✔
994
                                row -> new BoardLocationImpl(row, this), id,
3✔
995
                                address));
996
            }
997
        }
998

999
        @Override
1000
        public String getRootBoardBMPAddress() {
1001
            try (var conn = getConnection();
3✔
1002
                    var rootBMPaddr = conn.query(GET_ROOT_BMP_ADDRESS)) {
3✔
1003
                return conn.transaction(false, () -> rootBMPaddr.call1(
3✔
1004
                        string("address"), id).orElse(null));
3✔
1005
            }
1006
        }
1007

1008
        @Override
1009
        public List<Integer> getBoardNumbers() {
1010
            try (var conn = getConnection();
3✔
1011
                    var boardNumbers = conn.query(GET_BOARD_NUMBERS)) {
3✔
1012
                return conn.transaction(false, () -> boardNumbers.call(
3✔
1013
                        integer("board_num"), id));
3✔
1014
            }
1015
        }
1016

1017
        @Override
1018
        public List<BoardCoords> getDeadBoards() {
1019
            // Assume that the list doesn't change for the duration of this obj
1020
            synchronized (Spalloc.this) {
3✔
1021
                var down = downBoardsCache.get(name);
3✔
1022
                if (nonNull(down)) {
3✔
1023
                    return copy(down);
3✔
1024
                }
1025
            }
3✔
1026
            try (var conn = getConnection();
3✔
1027
                    var boardNumbers = conn.query(GET_DEAD_BOARDS)) {
3✔
1028
                var downBoards = conn.transaction(false,
3✔
1029
                        () -> boardNumbers.call(
3✔
1030
                                row -> new BoardCoords(row, false), id));
3✔
1031
                synchronized (Spalloc.this) {
3✔
1032
                    downBoardsCache.putIfAbsent(name, downBoards);
3✔
1033
                }
3✔
1034
                return copy(downBoards);
3✔
1035
            }
1036
        }
1037

1038
        @Override
1039
        public List<DownLink> getDownLinks() {
1040
            // Assume that the list doesn't change for the duration of this obj
1041
            synchronized (Spalloc.this) {
3✔
1042
                var down = downLinksCache.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(getDeadLinks)) {
3✔
1049
                var downLinks = conn.transaction(false, () -> boardNumbers
3✔
1050
                        .call(Spalloc::makeDownLinkFromRow, id));
3✔
1051
                synchronized (Spalloc.this) {
3✔
1052
                    downLinksCache.putIfAbsent(name, downLinks);
3✔
1053
                }
3✔
1054
                return copy(downLinks);
3✔
1055
            }
1056
        }
1057

1058
        @Override
1059
        public List<Integer> getAvailableBoards() {
1060
            try (var conn = getConnection();
3✔
1061
                    var boardNumbers = conn
3✔
1062
                            .query(GET_AVAILABLE_BOARD_NUMBERS)) {
3✔
1063
                return conn.transaction(false, () -> boardNumbers.call(
3✔
1064
                        integer("board_num"), id));
3✔
1065
            }
1066
        }
1067

1068
        @Override
1069
        public int getId() {
1070
            return id;
3✔
1071
        }
1072

1073
        @Override
1074
        public String getName() {
1075
            return name;
3✔
1076
        }
1077

1078
        @Override
1079
        public Set<String> getTags() {
1080
            return tags;
3✔
1081
        }
1082

1083
        @Override
1084
        public int getWidth() {
1085
            return width;
3✔
1086
        }
1087

1088
        @Override
1089
        public int getHeight() {
1090
            return height;
3✔
1091
        }
1092

1093
        @Override
1094
        public boolean isInService() {
1095
            return inService;
3✔
1096
        }
1097

1098
        @Override
1099
        public String getBMPAddress(BMPCoords bmp) {
1100
            try (var conn = getConnection();
3✔
1101
                    var bmpAddr = conn.query(GET_BMP_ADDRESS)) {
3✔
1102
                return conn.transaction(false,
3✔
1103
                        () -> bmpAddr
1104
                                .call1(string("address"), id, bmp.getCabinet(),
3✔
1105
                                        bmp.getFrame()).orElse(null));
3✔
1106
            }
1107
        }
1108

1109
        @Override
1110
        public List<Integer> getBoardNumbers(BMPCoords bmp) {
1111
            try (var conn = getConnection();
3✔
1112
                    var boardNumbers = conn.query(GET_BMP_BOARD_NUMBERS)) {
3✔
1113
                return conn.transaction(false,
3✔
1114
                        () -> boardNumbers
3✔
1115
                                .call(integer("board_num"), id,
3✔
1116
                                        bmp.getCabinet(), bmp.getFrame()));
3✔
1117
            }
1118
        }
1119

1120
        @Override
1121
        public boolean equals(Object other) {
1122
            // Equality is defined exactly by the database ID
NEW
1123
            return (other instanceof MachineImpl)
×
1124
                    && (id == ((MachineImpl) other).id);
1125
        }
1126

1127
        @Override
1128
        public int hashCode() {
NEW
1129
            return id;
×
1130
        }
1131

1132
        @Override
1133
        public String toString() {
NEW
1134
            return "Machine(" + name + ")";
×
1135
        }
1136

1137
        private void retrieveWraps() {
NEW
1138
            try (var conn = getConnection();
×
NEW
1139
                    var getWraps = conn.query(GET_MACHINE_WRAPS)) {
×
1140
                /*
1141
                 * No locking; not too bothered which thread asks as result will
1142
                 * be the same either way
1143
                 */
NEW
1144
                lookedUpWraps =
×
NEW
1145
                        conn.transaction(false, () -> getWraps.call1(rs -> {
×
NEW
1146
                            hWrap = rs.getBoolean("horizontal_wrap");
×
NEW
1147
                            vWrap = rs.getBoolean("vertical_wrap");
×
NEW
1148
                            return true;
×
NEW
1149
                        }, id)).orElse(false);
×
1150
            }
NEW
1151
        }
×
1152

1153
        @Override
1154
        public boolean isHorizonallyWrapped() {
NEW
1155
            if (!lookedUpWraps) {
×
NEW
1156
                retrieveWraps();
×
1157
            }
NEW
1158
            return hWrap;
×
1159
        }
1160

1161
        @Override
1162
        public boolean isVerticallyWrapped() {
NEW
1163
            if (!lookedUpWraps) {
×
NEW
1164
                retrieveWraps();
×
1165
            }
NEW
1166
            return vWrap;
×
1167
        }
1168
    }
1169

1170
    private final class JobCollection implements Jobs {
1171
        @JsonIgnore
1172
        private final Epoch epoch;
1173

1174
        private final List<Job> jobs;
1175

1176
        private JobCollection(List<Job> jobs) {
3✔
1177
            this.jobs = jobs;
3✔
1178
            if (jobs.isEmpty()) {
3✔
1179
                epoch = null;
3✔
1180
            } else {
1181
                epoch = epochs.getJobsEpoch(
3✔
1182
                        jobs.stream().map(Job::getId).collect(toList()));
3✔
1183
            }
1184
        }
3✔
1185

1186
        @Override
1187
        public boolean waitForChange(Duration timeout) {
NEW
1188
            if (isNull(epoch)) {
×
NEW
1189
                return true;
×
1190
            }
1191
            try {
NEW
1192
                return epoch.waitForChange(timeout);
×
NEW
1193
            } catch (InterruptedException interrupted) {
×
NEW
1194
                currentThread().interrupt();
×
NEW
1195
                return false;
×
1196
            }
1197
        }
1198

1199
        /**
1200
         * Get the set of jobs changed.
1201
         *
1202
         * @param timeout
1203
         *            The timeout to wait for until something happens.
1204
         * @return The set of changed job identifiers.
1205
         */
1206
        @Override
1207
        public Collection<Integer> getChanged(Duration timeout) {
1208
            if (isNull(epoch)) {
3✔
1209
                return jobs.stream().map(Job::getId).collect(toSet());
3✔
1210
            }
1211
            try {
NEW
1212
                return epoch.getChanged(timeout);
×
NEW
1213
            } catch (InterruptedException interrupted) {
×
NEW
1214
                currentThread().interrupt();
×
NEW
1215
                return jobs.stream().map(Job::getId).collect(toSet());
×
1216
            }
1217
        }
1218

1219
        @Override
1220
        public List<Job> jobs() {
NEW
1221
            return copy(jobs);
×
1222
        }
1223

1224
        @Override
1225
        public List<Integer> ids() {
1226
            return jobs.stream().map(Job::getId).collect(toList());
3✔
1227
        }
1228
    }
1229

1230
    private final class BoardReportSQL extends AbstractSQL {
3✔
1231
        final Query findBoardByChip = conn.query(findBoardByJobChip);
3✔
1232

1233
        final Query findBoardByTriad = conn.query(findBoardByLogicalCoords);
3✔
1234

1235
        final Query findBoardPhys = conn.query(findBoardByPhysicalCoords);
3✔
1236

1237
        final Query findBoardNet = conn.query(findBoardByIPAddress);
3✔
1238

1239
        final Update insertReport = conn.update(INSERT_BOARD_REPORT);
3✔
1240

1241
        final Query getReported = conn.query(getReportedBoards);
3✔
1242

1243
        final Update setFunctioning = conn.update(SET_FUNCTIONING_FIELD);
3✔
1244

1245
        final Query getNamedMachine = conn.query(GET_NAMED_MACHINE);
3✔
1246

1247
        @Override
1248
        public void close() {
1249
            findBoardByChip.close();
3✔
1250
            findBoardByTriad.close();
3✔
1251
            findBoardPhys.close();
3✔
1252
            findBoardNet.close();
3✔
1253
            insertReport.close();
3✔
1254
            getReported.close();
3✔
1255
            setFunctioning.close();
3✔
1256
            getNamedMachine.close();
3✔
1257
            super.close();
3✔
1258
        }
3✔
1259
    }
1260

1261
    /** Used to assemble an issue-report email for sending. */
1262
    private static final class EmailBuilder {
1263
        /**
1264
         * More efficient than several String.format() calls, and much clearer
1265
         * than a mess of direct {@link StringBuilder} calls!
1266
         */
1267
        private final Formatter b = new Formatter(Locale.UK);
3✔
1268

1269
        private final int id;
1270

1271
        /**
1272
         * @param id
1273
         *            The job ID
1274
         */
1275
        EmailBuilder(int id) {
3✔
1276
            this.id = id;
3✔
1277
        }
3✔
1278

1279
        void header(String issue, int numBoards, String who) {
1280
            b.format("Issues \"%s\" with %d boards reported by %s\n\n", issue,
3✔
1281
                    numBoards, who);
3✔
1282
        }
3✔
1283

1284
        void chip(ReportedBoard board) {
NEW
1285
            b.format("\tBoard for job (%d) chip %s\n", //
×
NEW
1286
                    id, board.chip);
×
NEW
1287
        }
×
1288

1289
        void triad(ReportedBoard board) {
NEW
1290
            b.format("\tBoard for job (%d) board (X:%d,Y:%d,Z:%d)\n", //
×
NEW
1291
                    id, board.x, board.y, board.z);
×
NEW
1292
        }
×
1293

1294
        void phys(ReportedBoard board) {
NEW
1295
            b.format(
×
1296
                    "\tBoard for job (%d) board "
1297
                            + "[Cabinet:%d,Frame:%d,Board:%d]\n", //
NEW
1298
                    id, board.cabinet, board.frame, board.board);
×
NEW
1299
        }
×
1300

1301
        void ip(ReportedBoard board) {
1302
            b.format("\tBoard for job (%d) board (IP: %s)\n", //
3✔
1303
                    id, board.address);
3✔
1304
        }
3✔
1305

1306
        void issue(int issueId) {
1307
            b.format("\t\tAction: noted as issue #%d\n", //
3✔
1308
                    issueId);
3✔
1309
        }
3✔
1310

1311
        void footer(int numActions) {
NEW
1312
            b.format("\nSummary: %d boards taken out of service.\n",
×
NEW
1313
                    numActions);
×
NEW
1314
        }
×
1315

1316
        void serviceActionDone(Reported report) {
NEW
1317
            b.format(
×
1318
                    "\tAction: board (X:%d,Y:%d,Z:%d) (IP: %s) "
1319
                            + "taken out of service once not in use "
1320
                            + "(%d problems reported)\n",
NEW
1321
                    report.x, report.y, report.z,
×
NEW
1322
                    report.address, report.numReports);
×
NEW
1323
        }
×
1324

1325
        /** @return The assembled message body. */
1326
        @Override
1327
        public String toString() {
NEW
1328
            return b.toString();
×
1329
        }
1330
    }
1331

1332
    private final class JobImpl implements Job {
1333
        @JsonIgnore
1334
        private Epoch epoch;
1335

1336
        private final int id;
1337

1338
        private final int machineId;
1339

1340
        private Integer width;
1341

1342
        private Integer height;
1343

1344
        private Integer depth;
1345

1346
        private JobState state;
1347

1348
        /** If not {@code null}, the ID of the root board of the job. */
1349
        private Integer root;
1350

1351
        private ChipLocation chipRoot;
1352

1353
        private String owner;
1354

1355
        private String keepaliveHost;
1356

1357
        private Instant startTime;
1358

1359
        private Instant keepaliveTime;
1360

1361
        private Instant finishTime;
1362

1363
        private String deathReason;
1364

1365
        private byte[] request;
1366

1367
        private boolean partial;
1368

1369
        private MachineImpl cachedMachine;
1370

1371
        JobImpl(int id, int machineId) {
3✔
1372
            this.epoch = epochs.getJobsEpoch(id);
3✔
1373
            this.id = id;
3✔
1374
            this.machineId = machineId;
3✔
1375
            partial = true;
3✔
1376
        }
3✔
1377

1378
        JobImpl(int jobId, int machineId, JobState jobState,
1379
                Instant keepalive) {
1380
            this(jobId, machineId);
3✔
1381
            state = jobState;
3✔
1382
            keepaliveTime = keepalive;
3✔
1383
        }
3✔
1384

1385
        JobImpl(Connection conn, Row row) {
3✔
1386
            this.id = row.getInt("job_id");
3✔
1387
            this.machineId = row.getInt("machine_id");
3✔
1388
            width = row.getInteger("width");
3✔
1389
            height = row.getInteger("height");
3✔
1390
            depth = row.getInteger("depth");
3✔
1391
            root = row.getInteger("root_id");
3✔
1392
            owner = row.getString("owner");
3✔
1393
            if (nonNull(root)) {
3✔
1394
                try (var boardRoot = conn.query(GET_ROOT_OF_BOARD)) {
3✔
1395
                    chipRoot = boardRoot.call1(chip("root_x", "root_y"), root)
3✔
1396
                            .orElse(null);
3✔
1397
                }
1398
            }
1399
            state = row.getEnum("job_state", JobState.class);
3✔
1400
            keepaliveHost = row.getString("keepalive_host");
3✔
1401
            keepaliveTime = row.getInstant("keepalive_timestamp");
3✔
1402
            startTime = row.getInstant("create_timestamp");
3✔
1403
            finishTime = row.getInstant("death_timestamp");
3✔
1404
            deathReason = row.getString("death_reason");
3✔
1405
            request = row.getBytes("original_request");
3✔
1406
            partial = false;
3✔
1407

1408
            this.epoch = epochs.getJobsEpoch(id);
3✔
1409
        }
3✔
1410

1411
        /**
1412
         * Get the machine that this job is running on. May used a cached value.
1413
         * A transaction is required, but may be a read-only transaction.
1414
         *
1415
         * @param conn
1416
         *            The connection to the DB
1417
         * @return The overall machine handle.
1418
         */
1419
        private synchronized MachineImpl getJobMachine(Connection conn) {
1420
            if (cachedMachine == null || !cachedMachine.epoch.isValid()) {
3✔
1421
                cachedMachine = Spalloc.this.getMachine(machineId, true, conn)
3✔
1422
                        .orElseThrow();
3✔
1423
            }
1424
            return cachedMachine;
3✔
1425
        }
1426

1427
        @Override
1428
        public void access(String keepaliveAddress) {
1429
            if (partial) {
3✔
NEW
1430
                throw new PartialJobException();
×
1431
            }
1432
            try (var conn = getConnection();
3✔
1433
                    var keepAlive = conn.update(UPDATE_KEEPALIVE)) {
3✔
1434
                conn.transaction(() -> keepAlive.call(keepaliveAddress, id));
3✔
1435
            }
1436
        }
3✔
1437

1438
        @Override
1439
        public void destroy(String reason) {
1440
            if (partial) {
3✔
NEW
1441
                throw new PartialJobException();
×
1442
            }
1443
            powerController.destroyJob(id, reason);
3✔
1444
            rememberer.closeJob(id);
3✔
1445
        }
3✔
1446

1447
        @Override
1448
        public void setPower(boolean power) {
NEW
1449
            powerController.setPower(id, power ? ON : OFF, READY);
×
NEW
1450
        }
×
1451

1452
        @Override
1453
        public boolean waitForChange(Duration timeout) {
1454
            if (isNull(epoch)) {
3✔
NEW
1455
                return true;
×
1456
            }
1457
            try {
NEW
1458
                return epoch.waitForChange(timeout);
×
1459
            } catch (InterruptedException interrupted) {
3✔
1460
                currentThread().interrupt();
3✔
1461
                return false;
3✔
1462
            }
1463
        }
1464

1465
        @Override
1466
        public int getId() {
1467
            return id;
3✔
1468
        }
1469

1470
        @Override
1471
        public JobState getState() {
1472
            return state;
3✔
1473
        }
1474

1475
        @Override
1476
        public Instant getStartTime() {
1477
            return startTime;
3✔
1478
        }
1479

1480
        @Override
1481
        public Optional<Instant> getFinishTime() {
1482
            return Optional.ofNullable(finishTime);
3✔
1483
        }
1484

1485
        @Override
1486
        public Optional<String> getReason() {
1487
            return Optional.ofNullable(deathReason);
3✔
1488
        }
1489

1490
        @Override
1491
        public Optional<String> getKeepaliveHost() {
1492
            if (partial) {
3✔
NEW
1493
                return Optional.empty();
×
1494
            }
1495
            return Optional.ofNullable(keepaliveHost);
3✔
1496
        }
1497

1498
        @Override
1499
        public Instant getKeepaliveTimestamp() {
1500
            return keepaliveTime;
3✔
1501
        }
1502

1503
        @Override
1504
        public Optional<byte[]> getOriginalRequest() {
1505
            if (partial) {
3✔
NEW
1506
                return Optional.empty();
×
1507
            }
1508
            return Optional.ofNullable(request);
3✔
1509
        }
1510

1511
        @Override
1512
        public Optional<SubMachine> getMachine() {
1513
            if (isNull(root)) {
3✔
1514
                return Optional.empty();
3✔
1515
            }
1516
            return executeRead(conn -> Optional.of(new SubMachineImpl(conn)));
3✔
1517
        }
1518

1519
        @Override
1520
        public Optional<BoardLocation> whereIs(int x, int y) {
1521
            if (isNull(root)) {
3✔
NEW
1522
                return Optional.empty();
×
1523
            }
1524
            try (var conn = getConnection();
3✔
1525
                    var findBoard = conn.query(findBoardByJobChip)) {
3✔
1526
                return conn.transaction(false, () -> findBoard
3✔
1527
                        .call1(row -> new BoardLocationImpl(row,
3✔
1528
                                getJobMachine(conn)), id, root, x, y));
3✔
1529
            }
1530
        }
1531

1532
        // -------------------------------------------------------------
1533
        // Bad board report handling
1534

1535
        @Override
1536
        public String reportIssue(IssueReportRequest report, Permit permit) {
1537
            try (var q = new BoardReportSQL()) {
3✔
1538
                var email = new EmailBuilder(id);
3✔
1539
                var result = q.transaction(
3✔
1540
                        () -> reportIssue(report, permit, email, q));
3✔
1541
                emailSender.sendServiceMail(email);
3✔
1542
                for (var m : report.boards.stream()
3✔
1543
                        .map(b -> q.getNamedMachine.call1(
3✔
1544
                                r -> r.getInt("machine_id"), b.machine, true))
3✔
1545
                        .collect(toSet())) {
3✔
1546
                    if (m.isPresent()) {
3✔
NEW
1547
                        epochs.machineChanged(m.get());
×
1548
                    }
1549
                }
3✔
1550

1551
                return result;
3✔
NEW
1552
            } catch (ReportRollbackExn e) {
×
NEW
1553
                return e.getMessage();
×
1554
            }
1555
        }
1556

1557
        /**
1558
         * Report an issue with some boards and assemble the email to send. This
1559
         * may result in boards being taken out of service (i.e., no longer
1560
         * being available to be allocated; their current allocation will
1561
         * continue).
1562
         * <p>
1563
         * <strong>NB:</strong> The sending of the email sending is
1564
         * <em>outside</em> the transaction that this code is executed in.
1565
         *
1566
         * @param report
1567
         *            The report from the user.
1568
         * @param permit
1569
         *            Who the user is.
1570
         * @param email
1571
         *            The email we're assembling.
1572
         * @param q
1573
         *            SQL access queries.
1574
         * @return Summary of action taken message, to go to user.
1575
         * @throws ReportRollbackExn
1576
         *             If the report is bad somehow.
1577
         */
1578
        private String reportIssue(IssueReportRequest report, Permit permit,
1579
                EmailBuilder email, BoardReportSQL q) throws ReportRollbackExn {
1580
            email.header(report.issue, report.boards.size(), permit.name);
3✔
1581
            int userId = getUser(q.getConnection(), permit.name)
3✔
1582
                    .orElseThrow(() -> new ReportRollbackExn(
3✔
1583
                            "no such user: %s", permit.name));
1584
            for (var board : report.boards) {
3✔
1585
                addIssueReport(q, getJobBoardForReport(q, board, email),
3✔
1586
                        report.issue, userId, email);
1587
            }
3✔
1588
            return takeBoardsOutOfService(q, email).map(acted -> {
3✔
NEW
1589
                email.footer(acted);
×
NEW
1590
                return format("%d boards taken out of service", acted);
×
1591
            }).orElse("report noted");
3✔
1592
        }
1593

1594
        /**
1595
         * Convert a board locator (for an issue report) into a board ID.
1596
         *
1597
         * @param q
1598
         *            How to touch the DB
1599
         * @param board
1600
         *            What board are we talking about
1601
         * @param email
1602
         *            The email we are building.
1603
         * @return The board ID
1604
         * @throws ReportRollbackExn
1605
         *             If the board can't be converted to an ID
1606
         */
1607
        private int getJobBoardForReport(BoardReportSQL q, ReportedBoard board,
1608
                EmailBuilder email) throws ReportRollbackExn {
1609
            Problem r;
1610
            if (nonNull(board.chip)) {
3✔
NEW
1611
                r = q.findBoardByChip
×
NEW
1612
                        .call1(Problem::new, id, root, board.chip.getX(),
×
NEW
1613
                                board.chip.getY())
×
NEW
1614
                        .orElseThrow(() -> new ReportRollbackExn(board.chip));
×
NEW
1615
                email.chip(board);
×
1616
            } else if (nonNull(board.x)) {
3✔
NEW
1617
                r = q.findBoardByTriad
×
NEW
1618
                        .call1(Problem::new, machineId, board.x, board.y,
×
1619
                                board.z)
NEW
1620
                        .orElseThrow(() -> new ReportRollbackExn(
×
1621
                                "triad (%s,%s,%s) not in machine", board.x,
1622
                                board.y, board.z));
NEW
1623
                if (isNull(r.jobId) || id != r.jobId) {
×
NEW
1624
                    throw new ReportRollbackExn(
×
1625
                            "triad (%s,%s,%s) not allocated to job %d", board.x,
NEW
1626
                            board.y, board.z, id);
×
1627
                }
NEW
1628
                email.triad(board);
×
1629
            } else if (nonNull(board.cabinet)) {
3✔
NEW
1630
                r = q.findBoardPhys
×
NEW
1631
                        .call1(Problem::new, machineId, board.cabinet,
×
1632
                                board.frame, board.board)
NEW
1633
                        .orElseThrow(() -> new ReportRollbackExn(
×
1634
                                "physical board [%s,%s,%s] not in machine",
1635
                                board.cabinet, board.frame, board.board));
NEW
1636
                if (isNull(r.jobId) || id != r.jobId) {
×
NEW
1637
                    throw new ReportRollbackExn(
×
1638
                            "physical board [%s,%s,%s] not allocated to job %d",
NEW
1639
                            board.cabinet, board.frame, board.board, id);
×
1640
                }
NEW
1641
                email.phys(board);
×
1642
            } else if (nonNull(board.address)) {
3✔
1643
                r = q.findBoardNet.call1(Problem::new, machineId, board.address)
3✔
1644
                        .orElseThrow(() -> new ReportRollbackExn(
3✔
1645
                                "board at %s not in machine", board.address));
1646
                if (isNull(r.jobId) || id != r.jobId) {
3✔
NEW
1647
                    throw new ReportRollbackExn(
×
1648
                            "board at %s not allocated to job %d",
NEW
1649
                            board.address, id);
×
1650
                }
1651
                email.ip(board);
3✔
1652
            } else {
NEW
1653
                throw new UnsupportedOperationException();
×
1654
            }
1655
            return r.boardId;
3✔
1656
        }
1657

1658
        /**
1659
         * Record a reported issue with a board.
1660
         *
1661
         * @param u
1662
         *            How to touch the DB
1663
         * @param boardId
1664
         *            What board has the issue?
1665
         * @param issue
1666
         *            What is the issue?
1667
         * @param userId
1668
         *            Who is doing the report?
1669
         * @param email
1670
         *            The email we are building.
1671
         */
1672
        private void addIssueReport(BoardReportSQL u, int boardId, String issue,
1673
                int userId, EmailBuilder email) {
1674
            u.insertReport.key(boardId, id, issue, userId)
3✔
1675
                    .ifPresent(email::issue);
3✔
1676
        }
3✔
1677

1678
        // -------------------------------------------------------------
1679

1680
        @Override
1681
        public Optional<ChipLocation> getRootChip() {
1682
            return Optional.ofNullable(chipRoot);
3✔
1683
        }
1684

1685
        @Override
1686
        public Optional<String> getOwner() {
1687
            if (partial) {
3✔
NEW
1688
                return Optional.empty();
×
1689
            }
1690
            return Optional.ofNullable(owner);
3✔
1691
        }
1692

1693
        @Override
1694
        public Optional<Integer> getWidth() {
1695
            return Optional.ofNullable(width);
3✔
1696
        }
1697

1698
        @Override
1699
        public Optional<Integer> getHeight() {
1700
            return Optional.ofNullable(height);
3✔
1701
        }
1702

1703
        @Override
1704
        public Optional<Integer> getDepth() {
1705
            return Optional.ofNullable(depth);
3✔
1706
        }
1707

1708
        @Override
1709
        public void rememberProxy(ProxyCore proxy) {
NEW
1710
            rememberer.rememberProxyForJob(id, proxy);
×
NEW
1711
        }
×
1712

1713
        @Override
1714
        public void forgetProxy(ProxyCore proxy) {
NEW
1715
            rememberer.removeProxyForJob(id, proxy);
×
NEW
1716
        }
×
1717

1718
        @Override
1719
        @SuppressWarnings("MustBeClosed")
1720
        public TransceiverInterface getTransceiver() throws IOException,
1721
                InterruptedException, SpinnmanException {
NEW
1722
            var mac = getMachine();
×
NEW
1723
            if (mac.isEmpty()) {
×
NEW
1724
                throw new IllegalStateException(
×
1725
                        "Job is not active!");
1726
            }
NEW
1727
            if (rememberer.isTransceiverForJob(id)) {
×
NEW
1728
                return rememberer.getTransceiverForJob(id);
×
1729
            }
NEW
1730
            var txrx = new Transceiver(InetAddress.getByName(
×
NEW
1731
                    mac.get().getConnections().get(0).getHostname()),
×
1732
                    MachineVersion.FIVE);
NEW
1733
            rememberer.setTransceiverForJob(id, txrx);
×
NEW
1734
            return txrx;
×
1735
        }
1736

1737
        @Override
1738
        public boolean equals(Object other) {
1739
            // Equality is defined exactly by the database ID
NEW
1740
            return (other instanceof JobImpl) && (id == ((JobImpl) other).id);
×
1741
        }
1742

1743
        @Override
1744
        public int hashCode() {
NEW
1745
            return id;
×
1746
        }
1747

1748
        @Override
1749
        public String toString() {
NEW
1750
            return format("Job(id=%s,dims=(%s,%s,%s),start=%s,finish=%s)", id,
×
1751
                    width, height, depth, startTime, finishTime);
1752
        }
1753

1754
        private final class SubMachineImpl implements SubMachine {
1755
            /** The machine that this sub-machine is part of. */
1756
            private final Machine machine;
1757

1758
            /** The root X coordinate of this sub-machine. */
1759
            private int rootX;
1760

1761
            /** The root Y coordinate of this sub-machine. */
1762
            private int rootY;
1763

1764
            /** The root Z coordinate of this sub-machine. */
1765
            private int rootZ;
1766

1767
            /** The connection details of this sub-machine. */
1768
            private List<ConnectionInfo> connections;
1769

1770
            /** The board locations of this sub-machine. */
1771
            private List<BoardCoordinates> boards;
1772

1773
            private List<Integer> boardIds;
1774

1775
            private SubMachineImpl(Connection conn) {
3✔
1776
                machine = getJobMachine(conn);
3✔
1777
                try (var getRootXY = conn.query(GET_ROOT_COORDS);
3✔
1778
                        var getBoardInfo = conn.query(GET_BOARD_CONNECT_INFO)) {
3✔
1779
                    getRootXY.call1(row -> {
3✔
1780
                        rootX = row.getInt("x");
3✔
1781
                        rootY = row.getInt("y");
3✔
1782
                        rootZ = row.getInt("z");
3✔
1783
                        // We have to return something,
1784
                        // but it doesn't matter what
1785
                        return true;
3✔
1786
                    }, root);
1787
                    int capacityEstimate = width * height;
3✔
1788
                    connections = new ArrayList<>(capacityEstimate);
3✔
1789
                    boards = new ArrayList<>(capacityEstimate);
3✔
1790
                    boardIds = new ArrayList<>(capacityEstimate);
3✔
1791
                    getBoardInfo.call(row -> {
3✔
1792
                        boardIds.add(row.getInt("board_id"));
3✔
1793
                        boards.add(new BoardCoordinates(row.getInt("x"),
3✔
1794
                                row.getInt("y"), row.getInt("z")));
3✔
1795
                        connections.add(new ConnectionInfo(
3✔
1796
                                relativeChipLocation(row.getInt("root_x"),
3✔
1797
                                        row.getInt("root_y")),
3✔
1798
                                row.getString("address")));
3✔
1799
                        // We have to return something,
1800
                        // but it doesn't matter what
1801
                        return true;
3✔
1802
                    }, id);
3✔
1803
                }
1804
            }
3✔
1805

1806
            private ChipLocation relativeChipLocation(int x, int y) {
1807
                x -= chipRoot.getX();
3✔
1808
                y -= chipRoot.getY();
3✔
1809
                // Allow for wrapping
1810
                if (x < 0) {
3✔
NEW
1811
                    x += machine.getWidth() * TRIAD_CHIP_SIZE;
×
1812
                }
1813
                if (y < 0) {
3✔
NEW
1814
                    y += machine.getHeight() * TRIAD_CHIP_SIZE;
×
1815
                }
1816
                return new ChipLocation(x, y);
3✔
1817
            }
1818

1819
            @Override
1820
            public Machine getMachine() {
1821
                return machine;
3✔
1822
            }
1823

1824
            @Override
1825
            public int getRootX() {
1826
                return rootX;
3✔
1827
            }
1828

1829
            @Override
1830
            public int getRootY() {
1831
                return rootY;
3✔
1832
            }
1833

1834
            @Override
1835
            public int getRootZ() {
1836
                return rootZ;
3✔
1837
            }
1838

1839
            @Override
1840
            public int getWidth() {
1841
                return width;
3✔
1842
            }
1843

1844
            @Override
1845
            public int getHeight() {
1846
                return height;
3✔
1847
            }
1848

1849
            @Override
1850
            public int getDepth() {
1851
                return depth;
3✔
1852
            }
1853

1854
            @Override
1855
            public List<ConnectionInfo> getConnections() {
1856
                return connections;
3✔
1857
            }
1858

1859
            @Override
1860
            public List<BoardCoordinates> getBoards() {
1861
                return boards;
3✔
1862
            }
1863

1864
            @Override
1865
            public PowerState getPower() {
1866
                try (var conn = getConnection();
3✔
1867
                        var power = conn.query(GET_SUM_BOARDS_POWERED)) {
3✔
1868
                    return conn.transaction(false,
3✔
1869
                            () -> power.call1(integer("total_on"), id)
3✔
1870
                                    .map(totalOn -> totalOn < boardIds.size()
3✔
1871
                                            ? OFF
3✔
NEW
1872
                                            : ON)
×
1873
                                    .orElse(null));
3✔
1874
                }
1875
            }
1876

1877
            @Override
1878
            public void setPower(PowerState ps) {
1879
                if (partial) {
3✔
NEW
1880
                    throw new PartialJobException();
×
1881
                }
1882
                powerController.setPower(id, ps, READY);
3✔
1883
            }
3✔
1884
        }
1885
    }
1886

1887
    /**
1888
     * Board location implementation. Does not retain database connections after
1889
     * creation.
1890
     *
1891
     * @author Donal Fellows
1892
     */
1893
    private final class BoardLocationImpl implements BoardLocation {
1894
        private JobImpl job;
1895

1896
        private final String machineName;
1897

1898
        private final int machineWidth;
1899

1900
        private final int machineHeight;
1901

1902
        private final ChipLocation chip;
1903

1904
        private final ChipLocation boardChip;
1905

1906
        private final BoardCoordinates logical;
1907

1908
        private final BoardPhysicalCoordinates physical;
1909

1910
        // Transaction is open
1911
        private BoardLocationImpl(Row row, Machine machine) {
3✔
1912
            machineName = row.getString("machine_name");
3✔
1913
            logical = new BoardCoordinates(row.getInt("x"), row.getInt("y"),
3✔
1914
                    row.getInt("z"));
3✔
1915
            physical = new BoardPhysicalCoordinates(row.getInt("cabinet"),
3✔
1916
                    row.getInt("frame"), row.getInteger("board_num"));
3✔
1917
            chip = row.getChip("chip_x", "chip_y");
3✔
1918
            machineWidth = machine.getWidth();
3✔
1919
            machineHeight = machine.getHeight();
3✔
1920
            var boardX = row.getInteger("board_chip_x");
3✔
1921
            if (nonNull(boardX)) {
3✔
1922
                boardChip = row.getChip("board_chip_x", "board_chip_y");
3✔
1923
            } else {
NEW
1924
                boardChip = chip;
×
1925
            }
1926

1927
            var jobId = row.getInteger("job_id");
3✔
1928
            if (nonNull(jobId)) {
3✔
1929
                job = new JobImpl(jobId, machine.getId());
3✔
1930
                job.chipRoot = row.getChip("job_root_chip_x",
3✔
1931
                        "job_root_chip_y");
1932
            }
1933
        }
3✔
1934

1935
        @Override
1936
        public ChipLocation getBoardChip() {
1937
            return boardChip;
3✔
1938
        }
1939

1940
        @Override
1941
        public ChipLocation getChipRelativeTo(ChipLocation rootChip) {
1942
            int x = chip.getX() - rootChip.getX();
3✔
1943
            if (x < 0) {
3✔
NEW
1944
                x += machineWidth * TRIAD_CHIP_SIZE;
×
1945
            }
1946
            int y = chip.getY() - rootChip.getY();
3✔
1947
            if (y < 0) {
3✔
NEW
1948
                y += machineHeight * TRIAD_CHIP_SIZE;
×
1949
            }
1950
            return new ChipLocation(x, y);
3✔
1951
        }
1952

1953
        @Override
1954
        public String getMachine() {
1955
            return machineName;
3✔
1956
        }
1957

1958
        @Override
1959
        public BoardCoordinates getLogical() {
1960
            return logical;
3✔
1961
        }
1962

1963
        @Override
1964
        public BoardPhysicalCoordinates getPhysical() {
1965
            return physical;
3✔
1966
        }
1967

1968
        @Override
1969
        public ChipLocation getChip() {
1970
            return chip;
3✔
1971
        }
1972

1973
        @Override
1974
        public Job getJob() {
1975
            return job;
3✔
1976
        }
1977
    }
1978

1979
    static class PartialJobException extends IllegalStateException {
1980
        private static final long serialVersionUID = 2997856394666135483L;
1981

1982
        PartialJobException() {
NEW
1983
            super("partial job only");
×
NEW
1984
        }
×
1985
    }
1986
}
1987

1988
class ReportRollbackExn extends RuntimeException {
1989
    private static final long serialVersionUID = 1L;
1990

1991
    @FormatMethod
1992
    ReportRollbackExn(String msg, Object... args) {
NEW
1993
        super(format(msg, args));
×
NEW
1994
    }
×
1995

1996
    ReportRollbackExn(HasChipLocation chip) {
NEW
1997
        this("chip at (%d,%d) not in job's allocation", chip.getX(),
×
NEW
1998
                chip.getY());
×
NEW
1999
    }
×
2000
}
2001

2002
abstract class GroupsException extends RuntimeException {
2003
    private static final long serialVersionUID = 6607077117924279611L;
2004

2005
    GroupsException(String message) {
NEW
2006
        super(message);
×
NEW
2007
    }
×
2008

2009
    GroupsException(String message, Throwable cause) {
NEW
2010
        super(message, cause);
×
NEW
2011
    }
×
2012
}
2013

2014
class NoSuchGroupException extends GroupsException {
2015
    private static final long serialVersionUID = 5193818294198205503L;
2016

2017
    @FormatMethod
2018
    NoSuchGroupException(String msg, Object... args) {
NEW
2019
        super(format(msg, args));
×
NEW
2020
    }
×
2021
}
2022

2023
class MultipleGroupsException extends GroupsException {
2024
    private static final long serialVersionUID = 6284332340565334236L;
2025

2026
    @FormatMethod
2027
    MultipleGroupsException(String msg, Object... args) {
NEW
2028
        super(format(msg, args));
×
NEW
2029
    }
×
2030
}
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