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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

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

Pull #658

github

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

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

8373 of 22997 relevant lines covered (36.41%)

0.36 hits per line

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

3.93
/SpiNNaker-comms/src/main/java/uk/ac/manchester/spinnaker/alloc/client/SpallocClientFactory.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.client;
17

18
import static com.fasterxml.jackson.databind.PropertyNamingStrategies.KEBAB_CASE;
19
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
20
import static java.lang.String.format;
21
import static java.lang.Thread.sleep;
22
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
23
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
24
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
25
import static java.net.URLEncoder.encode;
26
import static java.nio.charset.StandardCharsets.UTF_8;
27
import static java.util.Collections.synchronizedMap;
28
import static java.util.Objects.isNull;
29
import static java.util.Objects.nonNull;
30
import static java.util.stream.Collectors.joining;
31
import static java.util.stream.Collectors.toList;
32
import static org.apache.commons.io.IOUtils.readLines;
33
import static org.slf4j.LoggerFactory.getLogger;
34
import static uk.ac.manchester.spinnaker.alloc.client.ClientUtils.asDir;
35
import static uk.ac.manchester.spinnaker.utils.InetFactory.getByNameQuietly;
36
import static uk.ac.manchester.spinnaker.machine.ChipLocation.ZERO_ZERO;
37

38
import java.io.FileNotFoundException;
39
import java.io.IOException;
40
import java.io.InputStream;
41
import java.io.OutputStreamWriter;
42
import java.net.HttpURLConnection;
43
import java.net.Inet4Address;
44
import java.net.InetAddress;
45
import java.net.URI;
46
import java.util.ArrayList;
47
import java.util.Collection;
48
import java.util.HashMap;
49
import java.util.List;
50
import java.util.Map;
51
import java.util.stream.Stream;
52

53
import org.apache.commons.io.IOUtils;
54
import org.slf4j.Logger;
55

56
import com.fasterxml.jackson.databind.json.JsonMapper;
57
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
58
import com.google.errorprone.annotations.MustBeClosed;
59
import com.google.errorprone.annotations.concurrent.GuardedBy;
60

61
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.Job;
62
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.Machine;
63
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.SpallocException;
64
import uk.ac.manchester.spinnaker.connections.model.Connection;
65
import uk.ac.manchester.spinnaker.machine.ChipLocation;
66
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
67
import uk.ac.manchester.spinnaker.machine.MachineVersion;
68
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
69
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
70
import uk.ac.manchester.spinnaker.messages.model.Version;
71
import uk.ac.manchester.spinnaker.storage.ProxyInformation;
72
import uk.ac.manchester.spinnaker.transceiver.SpinnmanException;
73
import uk.ac.manchester.spinnaker.transceiver.TransceiverInterface;
74
import uk.ac.manchester.spinnaker.utils.Daemon;
75

76
/**
77
 * A factory for clients to connect to the Spalloc service.
78
 * <p>
79
 * <strong>Implementation Note:</strong> Neither this class nor the client
80
 * classes it creates maintain state that needs to be closed explicitly
81
 * <em>except</em> for
82
 * {@linkplain SpallocClient.Job#getTransceiver() transceivers}, as transceivers
83
 * usually need to be closed.
84
 *
85
 * @author Donal Fellows
86
 */
87
public class SpallocClientFactory {
88
        private static final Logger log = getLogger(SpallocClientFactory.class);
1✔
89

90
        private static final String CONTENT_TYPE = "Content-Type";
91

92
        private static final String TEXT_PLAIN = "text/plain; charset=UTF-8";
93

94
        private static final String APPLICATION_JSON = "application/json";
95

96
        private static final String FORM_ENCODED =
97
                        "application/x-www-form-urlencoded";
98

99
        private static final URI KEEPALIVE = URI.create("keepalive");
1✔
100

101
        private static final URI MACHINE = URI.create("machine");
1✔
102

103
        private static final URI POWER = URI.create("power");
1✔
104

105
        private static final URI WAIT_FLAG = URI.create("?wait=true");
1✔
106

107
        // Amount to divide keepalive interval by to get actual keep alive delay
108
        private static final int KEEPALIVE_DIVIDER = 2;
109

110
        /** Used to convert to/from JSON. */
111
        static final JsonMapper JSON_MAPPER = JsonMapper.builder()
1✔
112
                        .findAndAddModules().disable(WRITE_DATES_AS_TIMESTAMPS)
1✔
113
                        .addModule(new JavaTimeModule())
1✔
114
                        .propertyNamingStrategy(KEBAB_CASE).build();
1✔
115

116
        private final URI baseUrl;
117

118
        /**
119
         * Cache of machines, which don't expire.
120
         */
121
        private static final Map<String, Machine> MACHINE_MAP =
1✔
122
                        synchronizedMap(new HashMap<>());
1✔
123

124
        /**
125
         * Create a factory that can talk to a given service.
126
         *
127
         * @param baseUrl
128
         *            Where the server is.
129
         */
130
        public SpallocClientFactory(URI baseUrl) {
×
131
                this.baseUrl = asDir(baseUrl);
×
132
        }
×
133

134
        /**
135
         * Get a handle to a job given its proxy access information (derived from a
136
         * database query).
137
         *
138
         * @param proxy
139
         *            The proxy information from the database. Handles {@code null}.
140
         * @return The job handle, or {@code null} if {@code proxy==null}.
141
         * @throws IOException
142
         *             If connecting to the job fails.
143
         */
144
        public static Job getJobFromProxyInfo(ProxyInformation proxy)
145
                        throws IOException {
146
                if (proxy == null) {
×
147
                        return null;
×
148
                }
149
                log.info("Using proxy {} for connections", proxy.spallocUrl());
×
150
                return new SpallocClientFactory(URI.create(proxy.spallocUrl()))
×
151
                                .getJob(proxy);
×
152
        }
153

154
        /**
155
         * Read an object from a stream.
156
         *
157
         * @param <T>
158
         *            The type of the object to read.
159
         * @param is
160
         *            The stream
161
         * @param cls
162
         *            The class of object to read.
163
         * @return The object
164
         * @throws IOException
165
         *             If an I/O error happens or the content on the stream can't be
166
         *             made into an instance of the given class.
167
         */
168
        static <T> T readJson(InputStream is, Class<T> cls) throws IOException {
169
                var json = IOUtils.toString(is, UTF_8);
×
170
                try {
171
                        return JSON_MAPPER.readValue(json, cls);
×
172
                } catch (IOException e) {
×
173
                        log.error("Error while reading json {}", json);
×
174
                        throw e;
×
175
                }
176
        }
177

178
        /**
179
         * Outputs a form to a connection in
180
         * {@code application/x-www-form-urlencoded} format.
181
         *
182
         * @param connection
183
         *            The connection. Must have the right verb set.
184
         * @param map
185
         *            The contents of the form.
186
         * @throws IOException
187
         *             If I/O fails.
188
         */
189
        static void writeForm(HttpURLConnection connection, Map<String, String> map)
190
                        throws IOException {
191
                var form = map.entrySet().stream()
×
192
                                .map(e -> e.getKey() + "=" + encode(e.getValue(), UTF_8))
×
193
                                .collect(joining("&"));
×
194

195
                connection.setDoOutput(true);
×
196
                connection.setRequestProperty(CONTENT_TYPE, FORM_ENCODED);
×
197
                try (var w =
×
198
                                new OutputStreamWriter(connection.getOutputStream(), UTF_8)) {
×
199
                        w.write(form);
×
200
                }
201
        }
×
202

203
        /**
204
         * Outputs an object to a connection in {@code application/json} format.
205
         *
206
         * @param connection
207
         *            The connection. Must have the right verb set.
208
         * @param object
209
         *            The object to write.
210
         * @throws IOException
211
         *             If I/O fails.
212
         */
213
        static void writeObject(HttpURLConnection connection, Object object)
214
                        throws IOException {
215
                connection.setDoOutput(true);
×
216
                connection.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
×
217
                try (var out = connection.getOutputStream()) {
×
218
                        JSON_MAPPER.writeValue(out, object);
×
219
                }
220
        }
×
221

222
        /**
223
         * Outputs a string to a connection in {@code text/plain} format.
224
         *
225
         * @param connection
226
         *            The connection. Must have the right verb set.
227
         * @param string
228
         *            The string to write.
229
         * @throws IOException
230
         *             If I/O fails.
231
         */
232
        static void writeString(HttpURLConnection connection, String string)
233
                        throws IOException {
234
                connection.setDoOutput(true);
×
235
                connection.setRequestProperty(CONTENT_TYPE, TEXT_PLAIN);
×
236
                try (var w = new OutputStreamWriter(connection.getOutputStream(),
×
237
                                UTF_8)) {
238
                        w.write(string);
×
239
                }
240
        }
×
241

242
        /**
243
         * Checks for errors in the response.
244
         *
245
         * @param conn
246
         *            The HTTP connection
247
         * @param errorMessage
248
         *            The message to use on error (describes what did not work at a
249
         *            higher level)
250
         * @return The input stream so any non-error response content can be
251
         *         obtained.
252
         * @throws IOException
253
         *             If things go wrong with comms.
254
         * @throws FileNotFoundException
255
         *             on a {@link HttpURLConnection#HTTP_NOT_FOUND}
256
         * @throws SpallocException
257
         *             on other server errors
258
         */
259
        @MustBeClosed
260
        static InputStream checkForError(HttpURLConnection conn,
261
                        String errorMessage) throws IOException {
262
                if (conn.getResponseCode() == HTTP_NOT_FOUND) {
×
263
                        // Special case
264
                        throw new FileNotFoundException(errorMessage);
×
265
                }
266
                if (conn.getResponseCode() >= HTTP_BAD_REQUEST) {
×
267
                        throw new SpallocException(conn.getErrorStream(),
×
268
                                        conn.getResponseCode());
×
269
                }
270
                return conn.getInputStream();
×
271
        }
272

273
        /**
274
         * Create a client and log in.
275
         *
276
         * @param username
277
         *            The username to log in with.
278
         * @param password
279
         *            The password to log in with.
280
         * @return The client API for the given server.
281
         * @throws IOException
282
         *             If the server doesn't respond or logging in fails.
283
         */
284
        public SpallocClient login(String username, String password)
285
                        throws IOException {
286
                var s = new ClientSession(baseUrl, username, password);
×
287

288
                return new ClientImpl(s, s.discoverRoot());
×
289
        }
290

291
        /**
292
         * Get direct access to a Job.
293
         *
294
         * @param proxyInfo
295
         *            The information (URL, headers, cookies) about the job.
296
         * @return A job.
297
         * @throws IOException
298
         *             If there is an error communicating with the server.
299
         */
300
        private Job getJob(ProxyInformation proxyInfo) throws IOException {
301
                var u = URI.create(proxyInfo.jobUrl());
×
302
                var s = new ClientSession(baseUrl, proxyInfo.headers(),
×
303
                                proxyInfo.cookies());
×
304
                var c = new ClientImpl(s, s.discoverRoot());
×
305
                log.info("Connecting to job on {}", u);
×
306
                return c.job(u);
×
307
        }
308

309
        private abstract static sealed class Common
310
                        permits ClientImpl, JobImpl, MachineImpl {
311
                private final SpallocClient client;
312

313
                final Session s;
314

315
                Common(SpallocClient client, Session s) {
×
316
                        this.client = client != null ? client : (SpallocClient) this;
×
317
                        this.s = s;
×
318
                }
×
319

320
                final Machine getMachine(String name) throws IOException {
321
                        var m = MACHINE_MAP.get(name);
×
322
                        if (m == null) {
×
323
                                client.listMachines();
×
324
                                m = MACHINE_MAP.get(name);
×
325
                        }
326
                        if (m == null) {
×
327
                                throw new IOException("Machine " + name + " not found");
×
328
                        }
329
                        return m;
×
330
                }
331

332
                private WhereIs whereis(HttpURLConnection conn) throws IOException {
333
                        try (var is = checkForError(conn,
×
334
                                        "couldn't get board information")) {
335
                                if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
336
                                        throw new FileNotFoundException("machine not allocated");
×
337
                                }
338
                                return readJson(is, WhereIs.class);
×
339
                        } finally {
340
                                s.trackCookie(conn);
×
341
                        }
342
                }
343

344
                final WhereIs whereis(URI uri) throws IOException {
345
                        return s.withRenewal(() -> {
×
346
                                var conn = s.connection(uri);
×
347
                                var w = whereis(conn);
×
348
                                w.setMachineHandle(getMachine(w.getMachineName()));
×
349
                                w.clearMachineRef();
×
350
                                return w;
×
351
                        });
352
                }
353
        }
354

355
        private static final class ClientImpl extends Common
356
                        implements SpallocClient {
357
                private Version v;
358

359
                private URI jobs;
360

361
                private URI machines;
362

363
                private ClientImpl(Session s, RootInfo ri) {
364
                        super(null, s);
×
365
                        this.v = ri.version;
×
366
                        this.jobs = asDir(ri.jobsURI);
×
367
                        this.machines = asDir(ri.machinesURI);
×
368
                }
×
369

370
                @Override
371
                public Version getVersion() {
372
                        return v;
×
373
                }
374

375
                /**
376
                 * Slightly convoluted class to fetch jobs. The complication means we
377
                 * get the initial failure exception nice and early, while we're ready
378
                 * for it. This code would be quite a lot simpler if we didn't want to
379
                 * get the exception during construction.
380
                 */
381
                private class JobLister extends ListFetchingIter<URI> {
382
                        private URI next;
383

384
                        private List<URI> first;
385

386
                        JobLister(URI initial) throws IOException {
×
387
                                var first = getJobList(s.connection(initial));
×
388
                                next = first.next;
×
389
                                this.first = first.jobs;
×
390
                        }
×
391

392
                        private Jobs getJobList(HttpURLConnection conn) throws IOException {
393
                                try (var is = checkForError(conn, "couldn't list jobs")) {
×
394
                                        return readJson(is, Jobs.class);
×
395
                                } finally {
396
                                        s.trackCookie(conn);
×
397
                                }
398
                        }
399

400
                        @Override
401
                        List<URI> fetchNext() throws IOException {
402
                                if (nonNull(first)) {
×
403
                                        try {
404
                                                return first;
×
405
                                        } finally {
406
                                                first = null;
×
407
                                        }
408
                                }
409
                                var j = getJobList(s.connection(next));
×
410
                                next = j.next;
×
411
                                return j.jobs;
×
412
                        }
413

414
                        @Override
415
                        boolean canFetchMore() {
416
                                if (nonNull(first)) {
×
417
                                        return true;
×
418
                                }
419
                                return nonNull(next);
×
420
                        }
421
                }
422

423
                private Stream<Job> listJobs(URI flags) throws IOException {
424
                        var basicData = new JobLister(
×
425
                                        nonNull(flags) ? jobs.resolve(flags) : jobs);
×
426
                        return basicData.stream().flatMap(Collection::stream)
×
427
                                        .map(this::job);
×
428
                }
429

430
                @Override
431
                public List<Job> listJobs(boolean wait) throws IOException {
432
                        return s.withRenewal(() -> listJobs(WAIT_FLAG)).collect(toList());
×
433
                }
434

435
                @Override
436
                public Stream<Job> listJobsWithDeleted(boolean wait)
437
                                throws IOException {
438
                        var opts = new StringBuilder("?deleted=true");
×
439
                        if (wait) {
×
440
                                opts.append("&wait=true");
×
441
                        }
442
                        return s.withRenewal(() -> listJobs(URI.create(opts.toString())));
×
443
                }
444

445
                @Override
446
                public Job createJob(CreateJob createInstructions) throws IOException {
447
                        var uri = s.withRenewal(() -> {
×
448
                                var conn = s.connection(jobs, true);
×
449
                                writeObject(conn, createInstructions);
×
450
                                // Get the response entity... and discard it
451
                                try (var is = checkForError(conn, "job create failed")) {
×
452
                                        readLines(is, UTF_8);
×
453
                                        // But we do want the Location header
454
                                        return URI.create(conn.getHeaderField("Location"));
×
455
                                } finally {
456
                                        s.trackCookie(conn);
×
457
                                }
458
                        });
459
                        var job = job(uri);
×
460
                        job.startKeepalive(
×
461
                                        createInstructions.getKeepaliveInterval().toMillis()
×
462
                                        / KEEPALIVE_DIVIDER);
463
                        return job;
×
464
                }
465

466
                JobImpl job(URI uri) {
467
                        return new JobImpl(this, s, asDir(uri));
×
468
                }
469

470
                @Override
471
                public List<Machine> listMachines() throws IOException {
472
                        return s.withRenewal(() -> {
×
473
                                var conn = s.connection(machines);
×
474
                                try (var is = checkForError(conn, "list machines failed")) {
×
475
                                        var ms = readJson(is, Machines.class);
×
476
                                        // Assume we can cache this
477
                                        for (var bmd : ms.machines) {
×
478
                                                log.debug("Machine {} found", bmd.name);
×
479
                                                MACHINE_MAP.put(bmd.name,
×
480
                                                                new MachineImpl(this, s, bmd));
481
                                        }
×
482
                                        return new ArrayList<Machine>(MACHINE_MAP.values());
×
483
                                } finally {
484
                                        s.trackCookie(conn);
×
485
                                }
486
                        });
487
                }
488
        }
489

490
        private static final class JobImpl extends Common implements Job {
491
                private final URI uri;
492

493
                private volatile boolean dead;
494

495
                @GuardedBy("lock")
496
                private ProxyProtocolClient proxy;
497

498
                private final Object lock = new Object();
×
499

500
                JobImpl(SpallocClient client, Session session, URI uri) {
501
                        super(client, session);
×
502
                        this.uri = uri;
×
503
                        this.dead = false;
×
504
                }
×
505

506
                @Override
507
                public JobDescription describe(boolean wait) throws IOException {
508
                        return s.withRenewal(() -> {
×
509
                                var conn = wait ? s.connection(uri, WAIT_FLAG)
×
510
                                                : s.connection(uri);
×
511
                                try (var is = checkForError(conn, "couldn't get job state")) {
×
512
                                        return readJson(is, JobDescription.class);
×
513
                                } finally {
514
                                        s.trackCookie(conn);
×
515
                                }
516
                        });
517
                }
518

519
                @Override
520
                public void keepalive() throws IOException {
521
                        s.withRenewal(() -> {
×
522
                                var conn = s.connection(uri, KEEPALIVE, true);
×
523
                                conn.setRequestMethod("PUT");
×
524
                                writeString(conn, "alive");
×
525
                                try (var is = checkForError(conn, "couldn't keep job alive")) {
×
526
                                        return readLines(is, UTF_8);
×
527
                                        // Ignore the output
528
                                } finally {
529
                                        s.trackCookie(conn);
×
530
                                }
531
                        });
532
                }
×
533

534
                public void startKeepalive(long delayMs) {
535
                        if (dead) {
×
536
                                throw new IllegalStateException("job is already deleted");
×
537
                        }
538
                        var t = new Daemon(() -> {
×
539
                                try {
540
                                        while (true) {
541
                                                sleep(delayMs);
×
542
                                                if (dead) {
×
543
                                                        break;
×
544
                                                }
545
                                                keepalive();
×
546
                                        }
547
                                } catch (IOException e) {
×
548
                                        log.warn("failed to keep job alive for {}", this, e);
×
549
                                } catch (InterruptedException e) {
×
550
                                        // If interrupted, we're simply done
551
                                }
×
552
                        });
×
553
                        t.setName("keepalive for " + uri);
×
554
                        t.setUncaughtExceptionHandler((th, e) -> {
×
555
                                log.warn("unexpected exception in {}", th, e);
×
556
                        });
×
557
                        t.start();
×
558
                }
×
559

560
                @Override
561
                public void delete(String reason) throws IOException {
562
                        dead = true;
×
563
                        s.withRenewal(() -> {
×
564
                                var conn = s.connection(uri, "?reason=" + encode(reason, UTF_8),
×
565
                                                true);
566
                                conn.setRequestMethod("DELETE");
×
567
                                try (var is = checkForError(conn, "couldn't delete job")) {
×
568
                                        readLines(is, UTF_8);
×
569
                                        // Ignore the output
570
                                } finally {
571
                                        s.trackCookie(conn);
×
572
                                }
573
                                return this;
×
574
                        });
575
                        synchronized (lock) {
×
576
                                if (haveProxy()) {
×
577
                                        proxy.close();
×
578
                                        proxy = null;
×
579
                                }
580
                        }
×
581
                }
×
582

583
                @Override
584
                public AllocatedMachine machine() throws IOException {
585
                        var am = s.withRenewal(() -> {
×
586
                                var conn = s.connection(uri, MACHINE);
×
587
                                try (var is = checkForError(conn,
×
588
                                                "couldn't get allocation description")) {
589
                                        if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
590
                                                throw new IOException("machine not allocated");
×
591
                                        }
592
                                        return readJson(is, AllocatedMachine.class);
×
593
                                } finally {
594
                                        s.trackCookie(conn);
×
595
                                }
596
                        });
597
                        am.setMachine(getMachine(am.getMachineName()));
×
598
                        return am;
×
599
                }
600

601
                @Override
602
                public boolean getPower() throws IOException {
603
                        return s.withRenewal(() -> {
×
604
                                var conn = s.connection(uri, POWER);
×
605
                                try (var is = checkForError(conn, "couldn't get power state")) {
×
606
                                        if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
607
                                                throw new IOException("machine not allocated");
×
608
                                        }
609
                                        return "ON".equals(readJson(is, Power.class).power);
×
610
                                } finally {
611
                                        s.trackCookie(conn);
×
612
                                }
613
                        });
614
                }
615

616
                @Override
617
                public boolean setPower(boolean switchOn) throws IOException {
618
                        var power = new Power();
×
619
                        power.power = (switchOn ? "ON" : "OFF");
×
620
                        boolean powered = s.withRenewal(() -> {
×
621
                                var conn = s.connection(uri, POWER, true);
×
622
                                conn.setRequestMethod("PUT");
×
623
                                writeObject(conn, power);
×
624
                                try (var is = checkForError(conn, "couldn't set power state")) {
×
625
                                        if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
626
                                                throw new IOException("machine not allocated");
×
627
                                        }
628
                                        return "ON".equals(readJson(is, Power.class).power);
×
629
                                } finally {
630
                                        s.trackCookie(conn);
×
631
                                }
632
                        });
633
                        if (!powered) {
×
634
                                // If someone turns the power off, close the proxy
635
                                synchronized (lock) {
×
636
                                        if (haveProxy()) {
×
637
                                                proxy.close();
×
638
                                                proxy = null;
×
639
                                        }
640
                                }
×
641
                        }
642
                        return powered;
×
643
                }
644

645
                @Override
646
                public WhereIs whereIs(HasChipLocation chip) throws IOException {
647
                        return whereis(uri.resolve(
×
648
                                        format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
649
                }
650

651
                @GuardedBy("lock")
652
                private boolean haveProxy() {
653
                        return nonNull(proxy) && proxy.isOpen();
×
654
                }
655

656
                /**
657
                 * @return The websocket-based proxy.
658
                 * @throws IOException
659
                 *             if we can't connect
660
                 * @throws InterruptedException
661
                 *             if we're interrupted while connecting
662
                 */
663
                private ProxyProtocolClient getProxy()
664
                                throws IOException, InterruptedException {
665
                        synchronized (lock) {
×
666
                                if (haveProxy()) {
×
667
                                        return proxy;
×
668
                                }
669
                        }
×
670
                        var wssAddr = describe(false).getProxyAddress();
×
671
                        if (isNull(wssAddr)) {
×
672
                                throw new IOException("machine not allocated");
×
673
                        }
674
                        synchronized (lock) {
×
675
                                if (!haveProxy()) {
×
676
                                        proxy = s.withRenewal(() -> s.websocket(wssAddr));
×
677
                                }
678
                                return proxy;
×
679
                        }
680
                }
681

682
                @MustBeClosed
683
                @Override
684
                public TransceiverInterface getTransceiver()
685
                                throws IOException, InterruptedException, SpinnmanException {
686
                        var ws = getProxy();
×
687
                        var am = machine();
×
688
                        // TODO: We should know if the machine wraps around or not here...
689
                        var version = MachineVersion.TRIAD_NO_WRAPAROUND;
×
690
                        var conns = new ArrayList<Connection>();
×
691
                        var hostToChip = new HashMap<Inet4Address, ChipLocation>();
×
692
                        InetAddress bootChipAddress = null;
×
693
                        for (var bc : am.getConnections()) {
×
694
                                var chipAddr = getByNameQuietly(bc.getHostname());
×
695
                                var chipLoc = bc.getChip().asChipLocation();
×
696
                                conns.add(new ProxiedSCPConnection(chipLoc, ws, chipAddr));
×
697
                                hostToChip.put(chipAddr, bc.getChip());
×
698
                                if (chipLoc.equals(ZERO_ZERO)) {
×
699
                                        bootChipAddress = chipAddr;
×
700
                                }
701
                        }
×
702
                        if (bootChipAddress != null) {
×
703
                                conns.add(new ProxiedBootConnection(ws, bootChipAddress));
×
704
                        }
705
                        return new ProxiedTransceiver(version, conns, hostToChip, ws);
×
706
                }
707

708
                @Override
709
                public String toString() {
710
                        return "Job(" + uri + ")";
×
711
                }
712
        }
713

714
        private static final class MachineImpl extends Common implements Machine {
715
                private static final int TRIAD = 3;
716

717
                private final BriefMachineDescription bmd;
718

719
                private List<BoardCoords> deadBoards;
720

721
                private List<DeadLink> deadLinks;
722

723
                MachineImpl(SpallocClient client, Session session,
724
                                BriefMachineDescription bmd) {
725
                        super(client, session);
×
726
                        this.bmd = bmd;
×
727
                        this.deadBoards = List.copyOf(bmd.deadBoards);
×
728
                        this.deadLinks = List.copyOf(bmd.deadLinks);
×
729
                }
×
730

731
                @Override
732
                public String getName() {
733
                        return bmd.name;
×
734
                }
735

736
                @Override
737
                public List<String> getTags() {
738
                        return bmd.tags;
×
739
                }
740

741
                @Override
742
                public int getWidth() {
743
                        return bmd.width;
×
744
                }
745

746
                @Override
747
                public int getHeight() {
748
                        return bmd.height;
×
749
                }
750

751
                @Override
752
                public int getLiveBoardCount() {
753
                        return bmd.width * bmd.height * TRIAD - bmd.deadBoards.size();
×
754
                }
755

756
                @Override
757
                public List<BoardCoords> getDeadBoards() {
758
                        return deadBoards;
×
759
                }
760

761
                @Override
762
                public List<DeadLink> getDeadLinks() {
763
                        return deadLinks;
×
764
                }
765

766
                @Override
767
                public void waitForChange() throws IOException {
768
                        var nbmd = s.withRenewal(() -> {
×
769
                                var conn = s.connection(bmd.uri, WAIT_FLAG);
×
770
                                try (var is = checkForError(conn,
×
771
                                                "couldn't wait for state change")) {
772
                                        return readJson(is, BriefMachineDescription.class);
×
773
                                } finally {
774
                                        s.trackCookie(conn);
×
775
                                }
776
                        });
777
                        this.deadBoards = List.copyOf(nbmd.deadBoards);
×
778
                        this.deadLinks = List.copyOf(nbmd.deadLinks);
×
779
                }
×
780

781
                @Override
782
                public WhereIs getBoard(TriadCoords coords) throws IOException {
783
                        return whereis(
×
784
                                        bmd.uri.resolve(format("logical-board?x=%d&y=%d&z=%d",
×
785
                                                        coords.x(), coords.y(), coords.z())));
×
786
                }
787

788
                @Override
789
                public WhereIs getBoard(PhysicalCoords coords) throws IOException {
790
                        return whereis(bmd.uri.resolve(
×
791
                                        format("physical-board?cabinet=%d&frame=%d&board=%d",
×
792
                                                        coords.c(), coords.f(), coords.b())));
×
793
                }
794

795
                @Override
796
                public WhereIs getBoard(HasChipLocation chip) throws IOException {
797
                        return whereis(bmd.uri.resolve(
×
798
                                        format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
799
                }
800

801
                @Override
802
                public WhereIs getBoard(String address) throws IOException {
803
                        return whereis(bmd.uri.resolve(
×
804
                                        format("board-ip?address=%s", encode(address, UTF_8))));
×
805
                }
806
        }
807
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc