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

SpiNNakerManchester / JavaSpiNNaker / 13520271443

25 Feb 2025 11:34AM UTC coverage: 38.48% (-0.03%) from 38.51%
13520271443

push

github

rowleya
Fix tabs

675 of 2950 new or added lines in 17 files covered. (22.88%)

9 existing lines in 2 files now uncovered.

9182 of 23862 relevant lines covered (38.48%)

1.15 hits per line

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

4.27
/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.Integer.toUnsignedString;
21
import static java.lang.String.format;
22
import static java.lang.Thread.sleep;
23
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
24
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
25
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
26
import static java.net.URLEncoder.encode;
27
import static java.nio.charset.StandardCharsets.UTF_8;
28
import static java.nio.ByteOrder.LITTLE_ENDIAN;
29
import static java.util.Collections.synchronizedMap;
30
import static java.util.Objects.isNull;
31
import static java.util.Objects.nonNull;
32
import static java.util.stream.Collectors.joining;
33
import static java.util.stream.Collectors.toList;
34
import static org.apache.commons.io.IOUtils.readLines;
35
import static org.slf4j.LoggerFactory.getLogger;
36
import static uk.ac.manchester.spinnaker.alloc.client.ClientUtils.asDir;
37

38
import java.io.BufferedReader;
39
import java.io.FileNotFoundException;
40
import java.io.IOException;
41
import java.io.InputStream;
42
import java.io.InputStreamReader;
43
import java.io.OutputStreamWriter;
44
import java.net.HttpURLConnection;
45
import java.net.URI;
46
import java.net.URISyntaxException;
47
import java.nio.ByteBuffer;
48
import java.nio.channels.Channels;
49
import java.util.ArrayList;
50
import java.util.Collection;
51
import java.util.HashMap;
52
import java.util.List;
53
import java.util.Map;
54
import java.util.stream.Stream;
55

56
import org.apache.commons.io.IOUtils;
57
import org.slf4j.Logger;
58

59
import com.fasterxml.jackson.databind.json.JsonMapper;
60
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
61
import com.google.errorprone.annotations.MustBeClosed;
62
import com.google.errorprone.annotations.concurrent.GuardedBy;
63

64
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.Job;
65
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.Machine;
66
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.SpallocException;
67
import uk.ac.manchester.spinnaker.machine.ChipLocation;
68
import uk.ac.manchester.spinnaker.machine.CoreLocation;
69
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
70
import uk.ac.manchester.spinnaker.machine.MemoryLocation;
71
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
72
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
73
import uk.ac.manchester.spinnaker.machine.tags.IPTag;
74
import uk.ac.manchester.spinnaker.messages.model.Version;
75
import uk.ac.manchester.spinnaker.storage.ProxyInformation;
76
import uk.ac.manchester.spinnaker.transceiver.SpinnmanException;
77
import uk.ac.manchester.spinnaker.transceiver.TransceiverInterface;
78
import uk.ac.manchester.spinnaker.utils.Daemon;
79

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

94
        private static final String CONTENT_TYPE = "Content-Type";
95

96
        private static final String TEXT_PLAIN = "text/plain; charset=UTF-8";
97

98
        private static final String APPLICATION_JSON = "application/json";
99

100
        private static final String FORM_ENCODED =
101
                        "application/x-www-form-urlencoded";
102

103
        private static final URI KEEPALIVE = URI.create("keepalive");
3✔
104

105
        private static final URI MACHINE = URI.create("machine");
3✔
106

107
        private static final URI POWER = URI.create("power");
3✔
108

109
        private static final URI WAIT_FLAG = URI.create("?wait=true");
3✔
110

111
        private static final URI MEMORY = URI.create("memory");
3✔
112

113
        private static final URI FAST_DATA_WRITE = URI.create("fast-data-write");
3✔
114

115
        private static final URI FAST_DATA_READ = URI.create("fast-data-read");
3✔
116

117
        // Amount to divide keepalive interval by to get actual keep alive delay
118
        private static final int KEEPALIVE_DIVIDER = 2;
119

120
        /** Used to convert to/from JSON. */
121
        static final JsonMapper JSON_MAPPER = JsonMapper.builder()
3✔
122
                        .findAndAddModules().disable(WRITE_DATES_AS_TIMESTAMPS)
3✔
123
                        .addModule(new JavaTimeModule())
3✔
124
                        .propertyNamingStrategy(KEBAB_CASE).build();
3✔
125

126
        private final URI baseUrl;
127

128
        /**
129
         * Cache of machines, which don't expire.
130
         */
131
        private static final Map<String, Machine> MACHINE_MAP =
3✔
132
                        synchronizedMap(new HashMap<>());
3✔
133

134
        /**
135
         * Create a factory that can talk to a given service.
136
         *
137
         * @param baseUrl
138
         *            Where the server is.
139
         */
NEW
140
        public SpallocClientFactory(URI baseUrl) {
×
NEW
141
                this.baseUrl = asDir(baseUrl);
×
NEW
142
        }
×
143

144
        /**
145
         * Get a handle to a job given its proxy access information (derived from a
146
         * database query).
147
         *
148
         * @param proxy
149
         *            The proxy information from the database. Handles {@code null}.
150
         * @return The job handle, or {@code null} if {@code proxy==null}.
151
         * @throws IOException
152
         *             If connecting to the job fails.
153
         */
154
        public static Job getJobFromProxyInfo(ProxyInformation proxy)
155
                        throws IOException {
NEW
156
                if (proxy == null) {
×
NEW
157
                        return null;
×
158
                }
NEW
159
                log.info("Using proxy {} for connections", proxy.spallocUrl);
×
NEW
160
                return new SpallocClientFactory(URI.create(proxy.spallocUrl))
×
NEW
161
                                .getJob(proxy.jobUrl, proxy.headers, proxy.cookies);
×
162
        }
163

164
        /**
165
         * Read an object from a stream.
166
         *
167
         * @param <T>
168
         *            The type of the object to read.
169
         * @param is
170
         *            The stream
171
         * @param cls
172
         *            The class of object to read.
173
         * @return The object
174
         * @throws IOException
175
         *             If an I/O error happens or the content on the stream can't be
176
         *             made into an instance of the given class.
177
         */
178
        static <T> T readJson(InputStream is, Class<T> cls) throws IOException {
NEW
179
                BufferedReader streamReader = new BufferedReader(
×
180
                                new InputStreamReader(is, "UTF-8"));
NEW
181
                StringBuilder responseStrBuilder = new StringBuilder();
×
182

183
                String inputStr;
NEW
184
                while ((inputStr = streamReader.readLine()) != null) {
×
NEW
185
                        responseStrBuilder.append(inputStr);
×
186
                }
NEW
187
                String json = responseStrBuilder.toString();
×
188

189
                try {
NEW
190
                        return JSON_MAPPER.readValue(json, cls);
×
NEW
191
                } catch (IOException e) {
×
NEW
192
                        log.error("Error while reading json {}", json);
×
NEW
193
                        throw e;
×
194
                }
195
        }
196

197
        /**
198
         * Outputs a form to a connection in
199
         * {@code application/x-www-form-urlencoded} format.
200
         *
201
         * @param connection
202
         *            The connection. Must have the right verb set.
203
         * @param map
204
         *            The contents of the form.
205
         * @throws IOException
206
         *             If I/O fails.
207
         */
208
        static void writeForm(HttpURLConnection connection, Map<String, String> map)
209
                        throws IOException {
NEW
210
                var form = map.entrySet().stream()
×
NEW
211
                                .map(e -> e.getKey() + "=" + encode(e.getValue(), UTF_8))
×
NEW
212
                                .collect(joining("&"));
×
213

NEW
214
                connection.setDoOutput(true);
×
NEW
215
                connection.setRequestProperty(CONTENT_TYPE, FORM_ENCODED);
×
NEW
216
                try (var w =
×
NEW
217
                                new OutputStreamWriter(connection.getOutputStream(), UTF_8)) {
×
NEW
218
                        w.write(form);
×
219
                }
NEW
220
        }
×
221

222
        /**
223
         * Outputs an object to a connection in {@code application/json} format.
224
         *
225
         * @param connection
226
         *            The connection. Must have the right verb set.
227
         * @param object
228
         *            The object to write.
229
         * @throws IOException
230
         *             If I/O fails.
231
         */
232
        static void writeObject(HttpURLConnection connection, Object object)
233
                        throws IOException {
NEW
234
                connection.setDoOutput(true);
×
NEW
235
                connection.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
×
NEW
236
                try (var out = connection.getOutputStream()) {
×
NEW
237
                        JSON_MAPPER.writeValue(out, object);
×
238
                }
NEW
239
        }
×
240

241
        /**
242
         * Outputs a string to a connection in {@code text/plain} format.
243
         *
244
         * @param connection
245
         *            The connection. Must have the right verb set.
246
         * @param string
247
         *            The string to write.
248
         * @throws IOException
249
         *             If I/O fails.
250
         */
251
        static void writeString(HttpURLConnection connection, String string)
252
                        throws IOException {
NEW
253
                connection.setDoOutput(true);
×
NEW
254
                connection.setRequestProperty(CONTENT_TYPE, TEXT_PLAIN);
×
NEW
255
                try (var w = new OutputStreamWriter(connection.getOutputStream(),
×
256
                                UTF_8)) {
NEW
257
                        w.write(string);
×
258
                }
NEW
259
        }
×
260

261
        /**
262
         * Checks for errors in the response.
263
         *
264
         * @param conn
265
         *            The HTTP connection
266
         * @param errorMessage
267
         *            The message to use on error (describes what did not work at a
268
         *            higher level)
269
         * @return The input stream so any non-error response content can be
270
         *         obtained.
271
         * @throws IOException
272
         *             If things go wrong with comms.
273
         * @throws FileNotFoundException
274
         *             on a {@link HttpURLConnection#HTTP_NOT_FOUND}
275
         * @throws SpallocException
276
         *             on other server errors
277
         */
278
        @MustBeClosed
279
        static InputStream checkForError(HttpURLConnection conn,
280
                        String errorMessage) throws IOException {
NEW
281
                if (conn.getResponseCode() == HTTP_NOT_FOUND) {
×
282
                        // Special case
NEW
283
                        throw new FileNotFoundException(errorMessage);
×
284
                }
NEW
285
                if (conn.getResponseCode() >= HTTP_BAD_REQUEST) {
×
NEW
286
                        throw new SpallocException(conn.getErrorStream(),
×
NEW
287
                                        conn.getResponseCode());
×
288
                }
NEW
289
                return conn.getInputStream();
×
290
        }
291

292
        /**
293
         * Create a client and log in.
294
         *
295
         * @param username
296
         *            The username to log in with.
297
         * @param password
298
         *            The password to log in with.
299
         * @return The client API for the given server.
300
         * @throws IOException
301
         *             If the server doesn't respond or logging in fails.
302
         */
303
        public SpallocClient login(String username, String password)
304
                        throws IOException {
NEW
305
                var s = new ClientSession(baseUrl, username, password);
×
306

NEW
307
                return new ClientImpl(s, s.discoverRoot());
×
308
        }
309

310
        /**
311
         * Get direct access to a Job.
312
         *
313
         * @param uri
314
         *            The URI of the job
315
         * @param headers
316
         *            The headers to read authentication from.
317
         * @param cookies
318
         *            The cookies to read authentication from.
319
         * @return A job.
320
         * @throws IOException
321
         *             If there is an error communicating with the server.
322
         */
323
        public Job getJob(String uri, Map<String, String> headers,
324
                        Map<String, String> cookies) throws IOException {
NEW
325
                var u = URI.create(uri);
×
NEW
326
                var s = new ClientSession(baseUrl, headers, cookies);
×
NEW
327
                var c = new ClientImpl(s, s.discoverRoot());
×
NEW
328
                log.info("Connecting to job on {}", u);
×
NEW
329
                return c.job(u);
×
330
        }
331

332
        private abstract static class Common {
333
                private final SpallocClient client;
334

335
                final Session s;
336

NEW
337
                Common(SpallocClient client, Session s) {
×
NEW
338
                        this.client = client != null ? client : (SpallocClient) this;
×
NEW
339
                        this.s = s;
×
NEW
340
                }
×
341

342
                final Machine getMachine(String name) throws IOException {
NEW
343
                        Machine m = MACHINE_MAP.get(name);
×
NEW
344
                        if (m == null) {
×
NEW
345
                                client.listMachines();
×
NEW
346
                                m = MACHINE_MAP.get(name);
×
347
                        }
NEW
348
                        if (m == null) {
×
NEW
349
                                throw new IOException("Machine " + name + " not found");
×
350
                        }
NEW
351
                        return m;
×
352
                }
353

354
                private WhereIs whereis(HttpURLConnection conn) throws IOException {
NEW
355
                        try (var is = checkForError(conn,
×
356
                                        "couldn't get board information")) {
NEW
357
                                if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
NEW
358
                                        throw new FileNotFoundException("machine not allocated");
×
359
                                }
NEW
360
                                return readJson(is, WhereIs.class);
×
361
                        } finally {
NEW
362
                                s.trackCookie(conn);
×
363
                        }
364
                }
365

366
                final WhereIs whereis(URI uri) throws IOException {
NEW
367
                        return s.withRenewal(() -> {
×
NEW
368
                                var conn = s.connection(uri);
×
NEW
369
                                var w = whereis(conn);
×
NEW
370
                                w.setMachineHandle(getMachine(w.getMachineName()));
×
NEW
371
                                w.clearMachineRef();
×
NEW
372
                                return w;
×
373
                        });
374
                }
375
        }
376

377
        private static final class ClientImpl extends Common
378
                        implements SpallocClient {
379
                private Version v;
380

381
                private URI jobs;
382

383
                private URI machines;
384

385
                private ClientImpl(Session s, RootInfo ri) throws IOException {
NEW
386
                        super(null, s);
×
NEW
387
                        this.v = ri.version;
×
NEW
388
                        this.jobs = asDir(ri.jobsURI);
×
NEW
389
                        this.machines = asDir(ri.machinesURI);
×
NEW
390
                }
×
391

392
                @Override
393
                public Version getVersion() {
NEW
394
                        return v;
×
395
                }
396

397
                /**
398
                 * Slightly convoluted class to fetch jobs. The complication means we
399
                 * get the initial failure exception nice and early, while we're ready
400
                 * for it. This code would be quite a lot simpler if we didn't want to
401
                 * get the exception during construction.
402
                 */
403
                private class JobLister extends ListFetchingIter<URI> {
404
                        private URI next;
405

406
                        private List<URI> first;
407

NEW
408
                        JobLister(URI initial) throws IOException {
×
NEW
409
                                var first = getJobList(s.connection(initial));
×
NEW
410
                                next = first.next;
×
NEW
411
                                this.first = first.jobs;
×
NEW
412
                        }
×
413

414
                        private Jobs getJobList(HttpURLConnection conn) throws IOException {
NEW
415
                                try (var is = checkForError(conn, "couldn't list jobs")) {
×
NEW
416
                                        return readJson(is, Jobs.class);
×
417
                                } finally {
NEW
418
                                        s.trackCookie(conn);
×
419
                                }
420
                        }
421

422
                        @Override
423
                        List<URI> fetchNext() throws IOException {
NEW
424
                                if (nonNull(first)) {
×
425
                                        try {
NEW
426
                                                return first;
×
427
                                        } finally {
NEW
428
                                                first = null;
×
429
                                        }
430
                                }
NEW
431
                                var j = getJobList(s.connection(next));
×
NEW
432
                                next = j.next;
×
NEW
433
                                return j.jobs;
×
434
                        }
435

436
                        @Override
437
                        boolean canFetchMore() {
NEW
438
                                if (nonNull(first)) {
×
NEW
439
                                        return true;
×
440
                                }
NEW
441
                                return nonNull(next);
×
442
                        }
443
                }
444

445
                private Stream<Job> listJobs(URI flags) throws IOException {
NEW
446
                        var basicData = new JobLister(
×
NEW
447
                                        nonNull(flags) ? jobs.resolve(flags) : jobs);
×
NEW
448
                        return basicData.stream().flatMap(Collection::stream)
×
NEW
449
                                        .map(this::job);
×
450
                }
451

452
                @Override
453
                public List<Job> listJobs(boolean wait) throws IOException {
NEW
454
                        return s.withRenewal(() -> listJobs(WAIT_FLAG)).collect(toList());
×
455
                }
456

457
                @Override
458
                public Stream<Job> listJobsWithDeleted(boolean wait)
459
                                throws IOException {
NEW
460
                        var opts = new StringBuilder("?deleted=true");
×
NEW
461
                        if (wait) {
×
NEW
462
                                opts.append("&wait=true");
×
463
                        }
NEW
464
                        return s.withRenewal(() -> listJobs(URI.create(opts.toString())));
×
465
                }
466

467
                @Override
468
                public Job createJob(CreateJob createInstructions) throws IOException {
NEW
469
                        var uri = s.withRenewal(() -> {
×
NEW
470
                                var conn = s.connection(jobs, true);
×
NEW
471
                                writeObject(conn, createInstructions);
×
472
                                // Get the response entity... and discard it
NEW
473
                                try (var is = checkForError(conn, "job create failed")) {
×
NEW
474
                                        readLines(is, UTF_8);
×
475
                                        // But we do want the Location header
NEW
476
                                        return URI.create(conn.getHeaderField("Location"));
×
477
                                } finally {
NEW
478
                                        s.trackCookie(conn);
×
479
                                }
480
                        });
NEW
481
                        var job = job(uri);
×
NEW
482
                        job.startKeepalive(
×
NEW
483
                                        createInstructions.getKeepaliveInterval().toMillis()
×
484
                                        / KEEPALIVE_DIVIDER);
NEW
485
                        return job;
×
486
                }
487

488
                JobImpl job(URI uri) {
NEW
489
                        return new JobImpl(this, s, asDir(uri));
×
490
                }
491

492
                @Override
493
                public List<Machine> listMachines() throws IOException {
NEW
494
                        return s.withRenewal(() -> {
×
NEW
495
                                var conn = s.connection(machines);
×
NEW
496
                                try (var is = checkForError(conn, "list machines failed")) {
×
NEW
497
                                        var ms = readJson(is, Machines.class);
×
498
                                        // Assume we can cache this
NEW
499
                                        for (var bmd : ms.machines) {
×
NEW
500
                                                log.debug("Machine {} found", bmd.name);
×
NEW
501
                                                MACHINE_MAP.put(bmd.name,
×
502
                                                                new MachineImpl(this, s, bmd));
NEW
503
                                        }
×
NEW
504
                                        return new ArrayList<Machine>(MACHINE_MAP.values());
×
505
                                } finally {
NEW
506
                                        s.trackCookie(conn);
×
507
                                }
508
                        });
509
                }
510
        }
511

512
        private static final class JobImpl extends Common implements Job {
513
                private final URI uri;
514

515
                private volatile boolean dead;
516

517
                @GuardedBy("lock")
518
                private ProxyProtocolClient proxy;
519

NEW
520
                private final Object lock = new Object();
×
521

522
                JobImpl(SpallocClient client, Session session, URI uri) {
NEW
523
                        super(client, session);
×
NEW
524
                        this.uri = uri;
×
NEW
525
                        this.dead = false;
×
NEW
526
                }
×
527

528
                @Override
529
                public JobDescription describe(boolean wait) throws IOException {
NEW
530
                        return s.withRenewal(() -> {
×
NEW
531
                                var conn = wait ? s.connection(uri, WAIT_FLAG)
×
NEW
532
                                                : s.connection(uri);
×
NEW
533
                                try (var is = checkForError(conn, "couldn't get job state")) {
×
NEW
534
                                        return readJson(is, JobDescription.class);
×
535
                                } finally {
NEW
536
                                        s.trackCookie(conn);
×
537
                                }
538
                        });
539
                }
540

541
                @Override
542
                public void keepalive() throws IOException {
NEW
543
                        s.withRenewal(() -> {
×
NEW
544
                                var conn = s.connection(uri, KEEPALIVE, true);
×
NEW
545
                                conn.setRequestMethod("PUT");
×
NEW
546
                                writeString(conn, "alive");
×
NEW
547
                                try (var is = checkForError(conn, "couldn't keep job alive")) {
×
NEW
548
                                        return readLines(is, UTF_8);
×
549
                                        // Ignore the output
550
                                } finally {
NEW
551
                                        s.trackCookie(conn);
×
552
                                }
553
                        });
NEW
554
                }
×
555

556
                public void startKeepalive(long delayMs) {
NEW
557
                        if (dead) {
×
NEW
558
                                throw new IllegalStateException("job is already deleted");
×
559
                        }
NEW
560
                        var t = new Daemon(() -> {
×
561
                                try {
562
                                        while (true) {
NEW
563
                                                sleep(delayMs);
×
NEW
564
                                                if (dead) {
×
NEW
565
                                                        break;
×
566
                                                }
NEW
567
                                                keepalive();
×
568
                                        }
NEW
569
                                } catch (IOException e) {
×
NEW
570
                                        log.warn("failed to keep job alive for {}", this, e);
×
NEW
571
                                } catch (InterruptedException e) {
×
572
                                        // If interrupted, we're simply done
NEW
573
                                }
×
NEW
574
                        });
×
NEW
575
                        t.setName("keepalive for " + uri);
×
NEW
576
                        t.setUncaughtExceptionHandler((th, e) -> {
×
NEW
577
                                log.warn("unexpected exception in {}", th, e);
×
NEW
578
                        });
×
NEW
579
                        t.start();
×
NEW
580
                }
×
581

582
                @Override
583
                public void delete(String reason) throws IOException {
NEW
584
                        dead = true;
×
NEW
585
                        s.withRenewal(() -> {
×
NEW
586
                                var conn = s.connection(uri, "?reason=" + encode(reason, UTF_8),
×
587
                                                true);
NEW
588
                                conn.setRequestMethod("DELETE");
×
NEW
589
                                try (var is = checkForError(conn, "couldn't delete job")) {
×
NEW
590
                                        readLines(is, UTF_8);
×
591
                                        // Ignore the output
592
                                } finally {
NEW
593
                                        s.trackCookie(conn);
×
594
                                }
NEW
595
                                return this;
×
596
                        });
NEW
597
                        synchronized (lock) {
×
NEW
598
                                if (haveProxy()) {
×
NEW
599
                                        proxy.close();
×
NEW
600
                                        proxy = null;
×
601
                                }
NEW
602
                        }
×
NEW
603
                }
×
604

605
                @Override
606
                public AllocatedMachine machine() throws IOException {
NEW
607
                        var am = s.withRenewal(() -> {
×
NEW
608
                                var conn = s.connection(uri, MACHINE);
×
NEW
609
                                try (var is = checkForError(conn,
×
610
                                                "couldn't get allocation description")) {
NEW
611
                                        if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
NEW
612
                                                throw new IOException("machine not allocated");
×
613
                                        }
NEW
614
                                        return readJson(is, AllocatedMachine.class);
×
615
                                } finally {
NEW
616
                                        s.trackCookie(conn);
×
617
                                }
618
                        });
NEW
619
                        am.setMachine(getMachine(am.getMachineName()));
×
NEW
620
                        return am;
×
621
                }
622

623
                @Override
624
                public boolean getPower() throws IOException {
NEW
625
                        return s.withRenewal(() -> {
×
NEW
626
                                var conn = s.connection(uri, POWER);
×
NEW
627
                                try (var is = checkForError(conn, "couldn't get power state")) {
×
NEW
628
                                        if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
NEW
629
                                                throw new IOException("machine not allocated");
×
630
                                        }
NEW
631
                                        return "ON".equals(readJson(is, Power.class).power);
×
632
                                } finally {
NEW
633
                                        s.trackCookie(conn);
×
634
                                }
635
                        });
636
                }
637

638
                @Override
639
                public boolean setPower(boolean switchOn) throws IOException {
NEW
640
                        var power = new Power();
×
NEW
641
                        power.power = (switchOn ? "ON" : "OFF");
×
NEW
642
                        boolean powered = s.withRenewal(() -> {
×
NEW
643
                                var conn = s.connection(uri, POWER, true);
×
NEW
644
                                conn.setRequestMethod("PUT");
×
NEW
645
                                writeObject(conn, power);
×
NEW
646
                                try (var is = checkForError(conn, "couldn't set power state")) {
×
NEW
647
                                        if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
NEW
648
                                                throw new IOException("machine not allocated");
×
649
                                        }
NEW
650
                                        return "ON".equals(readJson(is, Power.class).power);
×
651
                                } finally {
NEW
652
                                        s.trackCookie(conn);
×
653
                                }
654
                        });
NEW
655
                        if (!powered) {
×
656
                                // If someone turns the power off, close the proxy
NEW
657
                                synchronized (lock) {
×
NEW
658
                                        if (haveProxy()) {
×
NEW
659
                                                proxy.close();
×
NEW
660
                                                proxy = null;
×
661
                                        }
NEW
662
                                }
×
663
                        }
NEW
664
                        return powered;
×
665
                }
666

667
                @Override
668
                public WhereIs whereIs(HasChipLocation chip) throws IOException {
NEW
669
                        return whereis(uri.resolve(
×
NEW
670
                                        format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
671
                }
672

673
                @GuardedBy("lock")
674
                private boolean haveProxy() {
NEW
675
                        return nonNull(proxy) && proxy.isOpen();
×
676
                }
677

678
                /**
679
                 * @return The websocket-based proxy.
680
                 * @throws IOException
681
                 *             if we can't connect
682
                 * @throws InterruptedException
683
                 *             if we're interrupted while connecting
684
                 */
685
                private ProxyProtocolClient getProxy()
686
                                throws IOException, InterruptedException {
NEW
687
                        synchronized (lock) {
×
NEW
688
                                if (haveProxy()) {
×
NEW
689
                                        return proxy;
×
690
                                }
NEW
691
                        }
×
NEW
692
                        var wssAddr = describe(false).getProxyAddress();
×
NEW
693
                        if (isNull(wssAddr)) {
×
NEW
694
                                throw new IOException("machine not allocated");
×
695
                        }
NEW
696
                        synchronized (lock) {
×
NEW
697
                                if (!haveProxy()) {
×
NEW
698
                                        proxy = s.withRenewal(() -> s.websocket(wssAddr));
×
699
                                }
NEW
700
                                return proxy;
×
701
                        }
702
                }
703

704
                @MustBeClosed
705
                @Override
706
                public TransceiverInterface getTransceiver()
707
                                throws IOException, InterruptedException, SpinnmanException {
NEW
708
                        var ws = getProxy();
×
NEW
709
                        return new ProxiedTransceiver(this, ws);
×
710
                }
711

712
                @Override
713
                public String toString() {
NEW
714
                        return "Job(" + uri + ")";
×
715
                }
716

717
                @Override
718
                public void writeMemory(HasChipLocation chip,
719
                                MemoryLocation baseAddress, ByteBuffer data)
720
                                throws IOException {
721
                        try {
NEW
722
                                s.withRenewal(() -> {
×
NEW
723
                                        var conn = s.connection(uri,
×
NEW
724
                                                        new URI(MEMORY + "?x=" + chip.getX()
×
NEW
725
                                                                        + "&y=" + chip.getY()
×
726
                                                                        + "&address="
NEW
727
                                                                        + toUnsignedString(baseAddress.address)),
×
728
                                                        true);
NEW
729
                                        conn.setDoOutput(true);
×
NEW
730
                                        conn.setRequestMethod("POST");
×
NEW
731
                                        conn.setRequestProperty(
×
732
                                                        "Content-Type", "application/octet-stream");
NEW
733
                                        try (var os = conn.getOutputStream();
×
NEW
734
                                                 var channel = Channels.newChannel(os)) {
×
NEW
735
                                                channel.write(data);
×
736
                                        }
NEW
737
                                        try (var is = checkForError(conn, "couldn't write memory")) {
×
738
                                                // Do Nothing
NEW
739
                                        }
×
NEW
740
                                        return null;
×
741
                                });
NEW
742
                        } catch (URISyntaxException e) {
×
NEW
743
                                throw new IOException(e);
×
NEW
744
                        }
×
NEW
745
                }
×
746

747
                @Override
748
                public ByteBuffer readMemory(HasChipLocation chip,
749
                                MemoryLocation baseAddress, int length)
750
                                throws IOException {
751
                        try {
NEW
752
                                return s.withRenewal(() -> {
×
NEW
753
                                        var conn = s.connection(uri,
×
NEW
754
                                                        new URI(MEMORY + "?x=" + chip.getX()
×
NEW
755
                                                                        + "&y=" + chip.getY()
×
756
                                                                        + "&address="
NEW
757
                                                                        + toUnsignedString(baseAddress.address)
×
758
                                                                        + "&size=" + length));
NEW
759
                                        conn.setRequestMethod("GET");
×
NEW
760
                                        conn.setRequestProperty(
×
761
                                                        "Accept", "application/octet-stream");
NEW
762
                                        try (var is = checkForError(conn, "couldn't read memory")) {
×
NEW
763
                                                var buffer = ByteBuffer.allocate(length);
×
NEW
764
                                                var channel = Channels.newChannel(is);
×
NEW
765
                                                IOUtils.readFully(channel, buffer);
×
NEW
766
                                                buffer.rewind();
×
NEW
767
                                                return buffer.asReadOnlyBuffer().order(LITTLE_ENDIAN);
×
768
                                        }
769
                                });
NEW
770
                        } catch (URISyntaxException e) {
×
NEW
771
                                throw new IOException(e);
×
772
                        }
773
                }
774

775
                @Override
776
                public void fastWriteData(CoreLocation gathererCore,
777
                                ChipLocation ethernetChip, String ethernetAddress,
778
                                IPTag iptag, HasChipLocation chip, MemoryLocation baseAddress,
779
                                ByteBuffer data) throws IOException {
780
                        try {
NEW
781
                                s.withRenewal(() -> {
×
NEW
782
                                        var conn = s.connection(uri,
×
783
                                                        new URI(FAST_DATA_WRITE
NEW
784
                                                                        + "?gather_x=" + gathererCore.getX()
×
NEW
785
                                                                        + "&gather_y=" + gathererCore.getY()
×
NEW
786
                                                                        + "&gather_p=" + gathererCore.getP()
×
NEW
787
                                                                        + "&eth_x=" + ethernetChip.getX()
×
NEW
788
                                                                        + "&eth_y=" + ethernetChip.getY()
×
789
                                                                        + "&eth_address=" + ethernetAddress
NEW
790
                                                                        + "&iptag=" + iptag.getTag()
×
NEW
791
                                                                        + "&x=" + chip.getX()
×
NEW
792
                                                                        + "&y=" + chip.getY()
×
793
                                                                        + "&address="
NEW
794
                                                                        + toUnsignedString(baseAddress.address)),
×
795
                                                        true);
NEW
796
                                        conn.setDoOutput(true);
×
NEW
797
                                        conn.setRequestMethod("POST");
×
NEW
798
                                        conn.setRequestProperty(
×
799
                                                        "Content-Type", "application/octet-stream");
NEW
800
                                        try (var os = conn.getOutputStream();
×
NEW
801
                                                 var channel = Channels.newChannel(os)) {
×
NEW
802
                                                channel.write(data);
×
803
                                        }
NEW
804
                                        try (var is = checkForError(conn,
×
805
                                                        "couldn't fast write memory")) {
806
                                                // Do Nothing
NEW
807
                                        }
×
NEW
808
                                        return null;
×
809
                                });
NEW
810
                        } catch (URISyntaxException e) {
×
NEW
811
                                throw new IOException(e);
×
NEW
812
                        }
×
NEW
813
                }
×
814
        }
815

816
        private static final class MachineImpl extends Common implements Machine {
817
                private static final int TRIAD = 3;
818

819
                private final BriefMachineDescription bmd;
820

821
                private List<BoardCoords> deadBoards;
822

823
                private List<DeadLink> deadLinks;
824

825
                MachineImpl(SpallocClient client, Session session,
826
                                BriefMachineDescription bmd) {
NEW
827
                        super(client, session);
×
NEW
828
                        this.bmd = bmd;
×
NEW
829
                        this.deadBoards = List.copyOf(bmd.deadBoards);
×
NEW
830
                        this.deadLinks = List.copyOf(bmd.deadLinks);
×
NEW
831
                }
×
832

833
                @Override
834
                public String getName() {
NEW
835
                        return bmd.name;
×
836
                }
837

838
                @Override
839
                public List<String> getTags() {
NEW
840
                        return bmd.tags;
×
841
                }
842

843
                @Override
844
                public int getWidth() {
NEW
845
                        return bmd.width;
×
846
                }
847

848
                @Override
849
                public int getHeight() {
NEW
850
                        return bmd.height;
×
851
                }
852

853
                @Override
854
                public int getLiveBoardCount() {
NEW
855
                        return bmd.width * bmd.height * TRIAD - bmd.deadBoards.size();
×
856
                }
857

858
                @Override
859
                public List<BoardCoords> getDeadBoards() {
NEW
860
                        return deadBoards;
×
861
                }
862

863
                @Override
864
                public List<DeadLink> getDeadLinks() {
NEW
865
                        return deadLinks;
×
866
                }
867

868
                @Override
869
                public void waitForChange() throws IOException {
NEW
870
                        var nbmd = s.withRenewal(() -> {
×
NEW
871
                                var conn = s.connection(bmd.uri, WAIT_FLAG);
×
NEW
872
                                try (var is = checkForError(conn,
×
873
                                                "couldn't wait for state change")) {
NEW
874
                                        return readJson(is, BriefMachineDescription.class);
×
875
                                } finally {
NEW
876
                                        s.trackCookie(conn);
×
877
                                }
878
                        });
NEW
879
                        this.deadBoards = List.copyOf(nbmd.deadBoards);
×
NEW
880
                        this.deadLinks = List.copyOf(nbmd.deadLinks);
×
NEW
881
                }
×
882

883
                @Override
884
                public WhereIs getBoard(TriadCoords coords) throws IOException {
NEW
885
                        return whereis(
×
NEW
886
                                        bmd.uri.resolve(format("logical-board?x=%d&y=%d&z=%d",
×
NEW
887
                                                        coords.x, coords.y, coords.z)));
×
888
                }
889

890
                @Override
891
                public WhereIs getBoard(PhysicalCoords coords) throws IOException {
NEW
892
                        return whereis(bmd.uri.resolve(
×
NEW
893
                                        format("physical-board?cabinet=%d&frame=%d&board=%d",
×
NEW
894
                                                        coords.c, coords.f, coords.b)));
×
895
                }
896

897
                @Override
898
                public WhereIs getBoard(HasChipLocation chip) throws IOException {
NEW
899
                        return whereis(bmd.uri.resolve(
×
NEW
900
                                        format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
901
                }
902

903
                @Override
904
                public WhereIs getBoard(String address) throws IOException {
NEW
905
                        return whereis(bmd.uri.resolve(
×
NEW
906
                                        format("board-ip?address=%s", encode(address, UTF_8))));
×
907
                }
908
        }
909
}
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