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

SpiNNakerManchester / JavaSpiNNaker / 15183574032

22 May 2025 09:56AM UTC coverage: 38.282% (-0.02%) from 38.303%
15183574032

push

github

rowleya
Debug

9200 of 24032 relevant lines covered (38.28%)

1.15 hits per line

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

26.27
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/allocator/QuotaManager.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.util.Objects.isNull;
19
import static org.slf4j.LoggerFactory.getLogger;
20
import static uk.ac.manchester.spinnaker.alloc.db.Row.integer;
21
import static uk.ac.manchester.spinnaker.alloc.db.Utils.isBusy;
22
import static uk.ac.manchester.spinnaker.alloc.nmpi.ResourceUsage.BOARD_SECONDS;
23
import static uk.ac.manchester.spinnaker.alloc.nmpi.ResourceUsage.CORE_HOURS;
24
import static uk.ac.manchester.spinnaker.alloc.model.GroupRecord.GroupType.COLLABRATORY;
25
import static uk.ac.manchester.spinnaker.alloc.security.TrustLevel.USER;
26

27
import java.util.List;
28
import java.util.Optional;
29

30
import javax.annotation.PostConstruct;
31
import javax.ws.rs.BadRequestException;
32

33
import org.slf4j.Logger;
34
import org.springframework.beans.factory.annotation.Autowired;
35
import org.springframework.dao.DataAccessException;
36
import org.springframework.scheduling.annotation.Scheduled;
37
import org.springframework.stereotype.Service;
38

39
import com.google.errorprone.annotations.RestrictedApi;
40

41
import uk.ac.manchester.spinnaker.alloc.ForTestingOnly;
42
import uk.ac.manchester.spinnaker.alloc.ServiceMasterControl;
43
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.QuotaProperties;
44
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
45
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Query;
46
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Update;
47
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
48
import uk.ac.manchester.spinnaker.alloc.db.Row;
49
import uk.ac.manchester.spinnaker.alloc.nmpi.Job;
50
import uk.ac.manchester.spinnaker.alloc.nmpi.JobResourceUpdate;
51
import uk.ac.manchester.spinnaker.alloc.nmpi.NMPIv3API;
52
import uk.ac.manchester.spinnaker.alloc.nmpi.Project;
53
import uk.ac.manchester.spinnaker.alloc.nmpi.ResourceUsage;
54
import uk.ac.manchester.spinnaker.alloc.nmpi.SessionRequest;
55
import uk.ac.manchester.spinnaker.alloc.nmpi.SessionResourceUpdate;
56
import uk.ac.manchester.spinnaker.alloc.nmpi.SessionResponse;
57

58
/**
59
 * Manages user quotas.
60
 *
61
 * @author Donal Fellows
62
 */
63
@Service
64
public class QuotaManager extends DatabaseAwareBean {
3✔
65
        private static final Logger log = getLogger(QuotaManager.class);
3✔
66

67
        /**
68
         * The status of the quote to request from the NMPI service.
69
         */
70
        private static final String STATUS_ACCEPTED = "accepted";
71

72
        /**
73
         * An approximation of cores per board.
74
         */
75
        private static final int APPROX_CORES_PER_BOARD = 48 * 16;
76

77
        /**
78
         * The number of seconds per hour.
79
         */
80
        private static final int SECONDS_PER_HOUR = 60 * 60;
81

82
        @Autowired
83
        private ServiceMasterControl control;
84

85
        @Autowired
86
        private QuotaProperties quotaProps;
87

88
        /** The wrapped NMPI proxy, bound to the API key. */
89
        private NMPI nmpi;
90

91
        /**
92
         * Make the NMPI access interface.
93
         */
94
        @PostConstruct
95
        private void createProxy() {
96
                var nmpiUrl = quotaProps.getNMPIUrl();
3✔
97
                if (!nmpiUrl.isEmpty()) {
3✔
98
                        nmpi = new NMPI(quotaProps);
×
99
                }
100
        }
3✔
101

102
        /**
103
         * Can the user (in a specific group) create another job at this point? If
104
         * not, they're currently out of resources.
105
         *
106
         * @param groupId
107
         *            What group will the job be accounted against.
108
         * @return True if they can make a job. False if they can't.
109
         */
110
        public boolean mayCreateJob(int groupId) {
111
                try (var sql = new CreateCheckSQL()) {
3✔
112
                        return sql.transactionRead(() -> sql.mayCreateJob(groupId));
3✔
113
                }
114
        }
115

116
        private final class CreateCheckSQL extends AbstractSQL {
3✔
117
                // TODO These should be combined (but one is an aggregate so...)
118
                private final Query getQuota = conn.query(GET_GROUP_QUOTA);
3✔
119

120
                private final Query getCurrentUsage = conn.query(GET_CURRENT_USAGE);
3✔
121

122
                @Override
123
                public void close() {
124
                        getQuota.close();
3✔
125
                        getCurrentUsage.close();
3✔
126
                        super.close();
3✔
127
                }
3✔
128

129
                private boolean mayCreateJob(int groupId) {
130
                        return getQuota.call1(result -> {
3✔
131
                                var quota = result.getInteger("quota");
3✔
132
                                log.debug("Group {} has quota {}", groupId, quota);
3✔
133
                                if (isNull(quota)) {
3✔
134
                                        return true;
×
135
                                }
136
                                // Quota is defined; check if current usage exceeds it
137
                                int usage = getCurrentUsage.call1(
3✔
138
                                                integer("current_usage"), groupId).orElse(0);
3✔
139
                                log.debug("Group {} has usage {}", groupId, usage);
3✔
140
                                // If board-seconds are left, we're good to go
141
                                return (quota > usage);
3✔
142
                        }, groupId).orElse(true);
3✔
143
                }
144
        }
145

146
        /**
147
         * Has the execution of a job remained within its group's resource
148
         * allocation at this point?
149
         *
150
         * @param jobId
151
         *            What job is consuming resources?
152
         * @return True if the job can continue to run. False if it can't.
153
         */
154
        public boolean mayLetJobContinue(int jobId) {
155
                try (var sql = new ContinueCheckSQL()) {
×
156
                        return sql.transactionRead(() -> sql.mayLetJobContinue(jobId));
×
157
                }
158
        }
159

160
        /**
161
         * Has the execution of a job exceeded its group's resource allocation at
162
         * this point?
163
         *
164
         * @param jobId
165
         *            What job is consuming resources?
166
         * @return False if the job should be killed. True otherwise.
167
         */
168
        public boolean shouldKillJob(int jobId) {
169
                return !mayLetJobContinue(jobId);
×
170
        }
171

172
        private final class ContinueCheckSQL extends AbstractSQL {
×
173
                private final Query getUsageAndQuota =
×
174
                                conn.query(GET_JOB_USAGE_AND_QUOTA);
×
175

176
                @Override
177
                public void close() {
178
                        getUsageAndQuota.close();
×
179
                        super.close();
×
180
                }
×
181

182
                private boolean mayLetJobContinue(int jobId) {
183
                        return getUsageAndQuota.call1(
×
184
                                        // If we have an entry, check if usage <= quota
185
                                        row -> row.getInt("quota_used") <= row.getInt("quota"),
×
186
                                        jobId)
×
187

188
                                        // Otherwise, we'll just allow it
189
                                        .orElse(true);
×
190
                }
191
        }
192

193
        /**
194
         * Adjust a group's quota.
195
         *
196
         * @param groupId
197
         *            Which group's quota to change
198
         * @param delta
199
         *            Amount to change by, in board-seconds
200
         * @return Information about what group's quota was adjusted and what it has
201
         *         become.
202
         */
203
        public Optional<AdjustedQuota> addQuota(int groupId, int delta) {
204
                try (var sql = new AdjustQuotaSQL()) {
×
205
                        return sql.transaction(() -> sql.adjustQuota(groupId, delta));
×
206
                }
207
        }
208

209
        /**
210
         * Describes the result of the {@link QuotaManager#addQuota(int,int)}
211
         * operation.
212
         *
213
         * @author Donal Fellows
214
         */
215
        public static final class AdjustedQuota {
216
                private final String name;
217

218
                private final Long quota;
219

220
                private AdjustedQuota(Row row) {
×
221
                        this.name = row.getString("group_name");
×
222
                        this.quota = row.getLong("quota");
×
223
                }
×
224

225
                /** @return The name of the group. */
226
                public String getName() {
227
                        return name;
×
228
                }
229

230
                /** @return The new quota of the group. */
231
                public Long getQuota() {
232
                        return quota;
×
233
                }
234
        }
235

236
        private final class AdjustQuotaSQL extends AbstractSQL {
×
237
                private final Update adjustQuota = conn.update(ADJUST_QUOTA);
×
238

239
                private final Query getQuota = conn.query(GET_GROUP_QUOTA);
×
240

241
                @Override
242
                public void close() {
243
                        adjustQuota.close();
×
244
                        super.close();
×
245
                }
×
246

247
                private Optional<AdjustedQuota> adjustQuota(int groupId, int delta) {
248
                        if (adjustQuota.call(delta, groupId) == 0) {
×
249
                                return Optional.empty();
×
250
                        }
251
                        return getQuota.call1(AdjustedQuota::new, groupId);
×
252
                }
253
        }
254

255
        /**
256
         * Consolidates usage from finished jobs onto quotas. Runs hourly.
257
         */
258
        @Scheduled(cron = "#{quotaProperties.consolidationSchedule}")
259
        public void consolidateQuotas() {
260
                if (control.isPaused()) {
×
261
                        return;
×
262
                }
263
                // Split off for testability
264
                try (var c = getConnection()) {
×
265
                        doConsolidate(c);
×
266
                } catch (DataAccessException e) {
×
267
                        if (isBusy(e)) {
×
268
                                log.info("database is busy; "
×
269
                                                + "will try job quota consolidation processing later");
270
                                return;
×
271
                        }
272
                        throw e;
×
273
                }
×
274
        }
×
275

276
        private void doConsolidate(Connection c) {
277
                try (var sql = new ConsolidateSQL(c)) {
3✔
278
                        sql.transaction(sql::consolidate);
3✔
279
                }
280
        }
3✔
281

282
        private class ConsolidateSQL extends AbstractSQL {
283
                private final Query getConsoldationTargets =
3✔
284
                                conn.query(GET_CONSOLIDATION_TARGETS);
3✔
285

286
                private final Update decrementQuota = conn.update(DECREMENT_QUOTA);
3✔
287

288
                private final Update markConsolidated = conn.update(MARK_CONSOLIDATED);
3✔
289

290
                ConsolidateSQL(Connection c) {
3✔
291
                        super(c);
3✔
292
                }
3✔
293

294
                @Override
295
                public void close() {
296
                        getConsoldationTargets.close();
3✔
297
                        decrementQuota.close();
3✔
298
                        markConsolidated.close();
3✔
299
                        super.close();
3✔
300
                }
3✔
301

302
                // Result is arbitrary and ignored
303
                private Void consolidate() {
304
                        for (var target : getConsoldationTargets.call(Target::new)) {
3✔
305
                                decrementQuota.call(target.quotaUsed, target.groupId);
3✔
306
                                markConsolidated.call(target.jobId);
3✔
307
                        }
3✔
308
                        return null;
3✔
309
                }
310

311
                private class Target {
312
                        final Object quotaUsed;
313

314
                        final int groupId;
315

316
                        final int jobId;
317

318
                        Target(Row row) {
3✔
319
                                quotaUsed = row.getObject("quota_used");
3✔
320
                                groupId = row.getInt("group_id");
3✔
321
                                jobId = row.getInt("job_id");
3✔
322
                        }
3✔
323
                }
324
        }
325

326
        private static class QuotaInfo {
327
                /** The size of quota remaining, in board-seconds. */
328
                final long quota;
329

330
                /** The units that the quota was measured in on the NMPI. */
331
                final String units;
332

333
                QuotaInfo(long quota, String units) {
×
334
                        this.quota = quota;
×
335
                        this.units = units;
×
336
                }
×
337
        }
338

339
        private QuotaInfo parseQuotaData(List<Project> projects) {
340
                log.debug("Parsing {} projects", projects.size());
×
341
                String units = null;
×
342
                long total = 0;
×
343
                for (var project : projects) {
×
344
                        log.debug("Project {} has {} quotas", project.getCollab(),
×
345
                                        project.getQuotas().size());
×
346
                        for (var quota : project.getQuotas()) {
×
347
                                if (!quota.getPlatform().equals(quotaProps.getNMPIPlaform())) {
×
348
                                        continue;
×
349
                                }
350
                                log.debug("Quota for {} = {} {} ({} used)", quota.getPlatform(),
×
351
                                                quota.getLimit(), quota.getUnits(), quota.getUsage());
×
352
                                if (units == null) {
×
353
                                        units = quota.getUnits();
×
354
                                } else if (!units.equals(quota.getUnits())) {
×
355
                                        throw new RuntimeException("Quotas have multiple units!");
×
356
                                }
357

358
                                switch (quota.getUnits()) {
×
359
                                case BOARD_SECONDS:
360
                                        total += (long) (quota.getLimit() - quota.getUsage());
×
361
                                        break;
×
362
                                case CORE_HOURS:
363
                                        total += toBoardSeconds(quota.getLimit())
×
364
                                                        - toBoardSeconds(quota.getUsage());
×
365
                                        break;
×
366
                                default:
367
                                        throw new RuntimeException(
×
368
                                                        "Unknown Quota units: " + quota.getUnits());
×
369
                                }
370
                        }
×
371
                }
×
372
                return new QuotaInfo(total, units);
×
373
        }
374

375
        final Optional<String> checkQuota(String collab) {
376
                // Read collab from NMPI; fail if not there
377
                var projects = nmpi.getProjects(STATUS_ACCEPTED, collab);
×
378
                var info = parseQuotaData(projects);
×
379

380
                log.debug("Setting quota of collab {} to {}", collab, info.quota);
×
381

382
                // Update quota in group for collab from NMPI
383
                try (var c = getConnection();
×
384
                                var setQuota = c.update(SET_COLLAB_QUOTA)) {
×
385
                        c.transaction(() -> setQuota.call(info.quota, collab));
×
386
                }
387

388
                if (info.quota > 0) {
×
389
                        return Optional.of(info.units);
×
390
                }
391
                return Optional.empty();
×
392
        }
393

394
        final SessionResponse createSession(String collab, String user) {
395
                var session = nmpi.createSession(collab, user);
×
396
                checkQuota(collab);
×
397
                return session;
×
398
        }
399

400
        void associateNMPISession(int jobId, int sessionId, String quotaUnits) {
401
                // Associate NMPI session with Job in the database
402
                try (var c = getConnection();
×
403
                                var setSession = c.update(SET_JOB_SESSION)) {
×
404
                        c.transaction(
×
405
                                        () -> setSession.call(jobId, sessionId, quotaUnits));
×
406
                }
407
        }
×
408

409
        private final class InflateUser extends AbstractSQL {
×
410
                private final Query getUserByName =
×
411
                                conn.query(GET_USER_DETAILS_BY_NAME);
×
412

413
                private final Query getGroupByName = conn.query(GET_GROUP_BY_NAME);
×
414

415
                private final Update createUser = conn.update(CREATE_USER);
×
416

417
                private final Update createGroup =
×
418
                                conn.update(CREATE_GROUP_IF_NOT_EXISTS);
×
419

420
                private final Update addUserToGroup = conn.update(ADD_USER_TO_GROUP);
×
421

422
                @Override
423
                public void close() {
424
                        getUserByName.close();
×
425
                        getGroupByName.close();
×
426
                        createUser.close();
×
427
                        createGroup.close();
×
428
                        addUserToGroup.close();
×
429
                        super.close();
×
430
                }
×
431

432
                private Optional<Integer> getUserByName(String user) {
433
                        return getUserByName.call1(r -> r.getInt("user_id"), user);
×
434
                }
435

436
                private Optional<Integer> getGroupByName(String group) {
437
                        return getGroupByName.call1(r -> r.getInt("group_id"), group);
×
438
                }
439

440
                private boolean createUser(String user) {
441
                        return createUser.call(user, null, USER, false, null) > 0;
×
442
                }
443

444
                private boolean createGroup(String group) {
445
                        return createGroup.call(group, 0.0, COLLABRATORY) > 0;
×
446
                }
447

448
                private boolean addUserToGroup(int userId, Optional<Integer> groupId) {
449
                        return addUserToGroup.call(userId, groupId) > 0;
×
450
                }
451

452
                /**
453
                 * Construct the user and group records if necessary and associate them.
454
                 *
455
                 * @param user
456
                 *            The user name.
457
                 * @param job
458
                 *            The NMPI job details containing the collabratory (group)
459
                 *            name.
460
                 */
461
                void inflateUser(String user, Job job) {
462
                        var userId = getUserByName(user).or(() -> {
×
463
                                /*
464
                                 * The user has never logged in directly, so we have to create
465
                                 * them.
466
                                 */
467
                                if (!createUser(user)) {
×
468
                                        log.warn("failed to make user: {}", user);
×
469
                                }
470
                                return getUserByName(user);
×
471
                        });
472

473
                        createGroup(job.getCollab());
×
474
                        var groupId = getGroupByName(job.getCollab());
×
475
                        addUserToGroup(userId.orElseThrow(() -> new IllegalStateException(
×
476
                                        "failed to find or create user")), groupId);
477
                }
×
478
        }
479

480
        final Optional<NMPIJobQuotaDetails> mayUseNMPIJob(String user,
481
                        int nmpiJobId) {
482
                // Read job from NMPI to get collab ID
483
                var job = nmpi.getJob(nmpiJobId);
×
484

485
                // If it is possible to run this job, we need to associate the user
486
                // with it because only special users can run jobs like this.
487
                try (var sql = new InflateUser()) {
×
488
                        sql.transaction(() -> sql.inflateUser(user, job));
×
489
                }
490

491
                // This is now a collab so check there instead
492
                var quotaUnits = checkQuota(job.getCollab());
×
493
                if (quotaUnits.isEmpty()) {
×
494
                        return Optional.empty();
×
495
                }
496

497
                return Optional.of(
×
498
                                new NMPIJobQuotaDetails(job.getCollab(), quotaUnits.get()));
×
499
        }
500

501
        static final class NMPIJobQuotaDetails {
502
                /** The collaboratory ID. */
503
                final String collabId;
504

505
                /** The units of the Quota. */
506
                final String quotaUnits;
507

508
                private NMPIJobQuotaDetails(String collabId, String quotaUnits) {
×
509
                        this.collabId = collabId;
×
510
                        this.quotaUnits = quotaUnits;
×
511
                }
×
512
        }
513

514
        void associateNMPIJob(int jobId, int nmpiJobId, String quotaUnits) {
515
                // Associate NMPI Job with job in database
516
                try (var c = getConnection();
×
517
                                var setNMPIJob = c.update(SET_JOB_NMPI_JOB)) {
×
518
                        c.transaction(() -> setNMPIJob.call(jobId, nmpiJobId, quotaUnits));
×
519
                }
520
        }
×
521

522
        /** Results of database queries. */
523
        private static final class FinishInfo {
524
                Optional<Long> quota;
525

526
                Optional<Session> session;
527

528
                Optional<NMPIJob> job;
529
        }
530

531
        private FinishInfo getFinishingInfo(int jobId) {
532
                try (var c = getConnection();
3✔
533
                                var getSession = c.query(GET_JOB_SESSION);
3✔
534
                                var getNMPIJob = c.query(GET_JOB_NMPI_JOB);
3✔
535
                                var getUsage = c.query(GET_JOB_USAGE_AND_QUOTA)) {
3✔
536
                        // Get the quota used
537
                        return c.transaction(false, () -> {
3✔
538
                                var i = new FinishInfo();
3✔
539
                                i.quota = getUsage.call1(r -> r.getLong("quota_used"), jobId);
3✔
540
                                i.session = getSession.call1(Session::new, jobId);
3✔
541
                                i.job = getNMPIJob.call1(NMPIJob::new, jobId);
3✔
542
                                return i;
3✔
543
                        });
544
                }
545
        }
546

547
        final void finishJob(int jobId) {
548
                // Get the information about the job from the DB
549
                var info = getFinishingInfo(jobId);
3✔
550

551
                // From here on, we don't touch the DB but we do touch the network
552

553
                if (!info.quota.isPresent()) {
3✔
554
                        // No quota? No update!
555
                        return;
×
556
                }
557

558
                // If job has associated session, update quota in session
559
                info.session.ifPresent(session -> {
3✔
560
                        try {
561
                                nmpi.setSessionStatusAndResources(session.id, "finished",
×
562
                                                getResourceUsage(info.quota.get(), session.quotaUnits));
×
563
                        } catch (BadRequestException e) {
×
564
                                log.error(e.getResponse().readEntity(String.class));
×
565
                                throw e;
×
566
                        }
×
567
                });
×
568

569
                // If job has associated NMPI job, update quota on NMPI job
570
                info.job.ifPresent(nmpiJob -> {
3✔
571
                        try {
572
                                nmpi.setJobResources(nmpiJob.id,
×
573
                                                getResourceUsage(info.quota.get(), nmpiJob.quotaUnits));
×
574
                        } catch (BadRequestException e) {
×
575
                                log.error(e.getResponse().readEntity(String.class));
×
576
                                throw e;
×
577
                        }
×
578
                });
×
579
        }
3✔
580

581
        private static final class Session {
582
                private int id;
583

584
                private String quotaUnits;
585

586
                private Session(Row r) {
×
587
                        this.id = r.getInt("session_id");
×
588
                        this.quotaUnits = r.getString("quota_units");
×
589
                }
×
590
        }
591

592
        private static final class NMPIJob {
593
                private int id;
594

595
                private String quotaUnits;
596

597
                private NMPIJob(Row r) {
×
598
                        this.id = r.getInt("nmpi_job_id");
×
599
                        this.quotaUnits = r.getString("quota_units");
×
600
                }
×
601
        }
602

603
        private static ResourceUsage getResourceUsage(long boardSeconds,
604
                        String units) {
605
                var resourceUsage = new ResourceUsage();
×
606
                resourceUsage.setUnits(units);
×
607
                if (units.equals(BOARD_SECONDS)) {
×
608
                        resourceUsage.setValue(boardSeconds);
×
609
                } else if (units.equals(CORE_HOURS)) {
×
610
                        resourceUsage.setValue(toCoreHours(boardSeconds));
×
611
                } else {
612
                        throw new IllegalArgumentException("Unknown units " + units);
×
613
                }
614
                return resourceUsage;
×
615
        }
616

617
        /**
618
         * Convert board-seconds to core-hours (approximately).
619
         *
620
         * @param boardSeconds
621
         *            The number of board-seconds to convert.
622
         * @return The number of board-hours, which may have fractional values.
623
         */
624
        private static double toCoreHours(long boardSeconds) {
625
                return ((double) (boardSeconds * APPROX_CORES_PER_BOARD))
×
626
                                / SECONDS_PER_HOUR;
627
        }
628

629
        /**
630
         * Convert core-hours to board-seconds (approximately).
631
         *
632
         * @param coreHours
633
         *            The number of core-hours to convert.
634
         * @return The integer number of board seconds.
635
         */
636
        private static long toBoardSeconds(double coreHours) {
637
                return (long) ((coreHours * SECONDS_PER_HOUR) / APPROX_CORES_PER_BOARD);
×
638
        }
639

640
        /**
641
         * Operations for testing only.
642
         *
643
         * @hidden
644
         */
645
        @ForTestingOnly
646
        interface TestAPI {
647
                /**
648
                 * Consolidates usage from finished jobs onto quotas.
649
                 *
650
                 * @param c
651
                 *            How to talk to the DB.
652
                 */
653
                void doConsolidate(Connection c);
654
        }
655

656
        /**
657
         * @return The test interface.
658
         * @deprecated This interface is just for testing.
659
         * @hidden
660
         */
661
        @ForTestingOnly
662
        @RestrictedApi(explanation = "just for testing", link = "index.html",
663
                        allowedOnPath = ".*/src/test/java/.*")
664
        @Deprecated
665
        TestAPI getTestAPI() {
666
                ForTestingOnly.Utils.checkForTestClassOnStack();
3✔
667
                return new TestAPI() {
3✔
668
                        @Override
669
                        public void doConsolidate(Connection c) {
670
                                QuotaManager.this.doConsolidate(c);
3✔
671
                        }
3✔
672
                };
673
        }
674
}
675

676
/**
677
 * Wrapper round the proxy and its API key. Does a bit of wrapping and
678
 * unwrapping of arguments and results.
679
 *
680
 * @author Donal Fellows
681
 */
682
final class NMPI {
683
        private final NMPIv3API proxy;
684

685
        private final String apiKey;
686

687
        private final String platform;
688

689
        NMPI(QuotaProperties quotaProps) {
×
690
                proxy = NMPIv3API.createClient(quotaProps.getNMPIUrl());
×
691
                apiKey = quotaProps.getNMPIApiKey();
×
692
                platform = quotaProps.getNMPIPlaform();
×
693
        }
×
694

695
        Job getJob(int jobId) {
696
                return proxy.getJob(apiKey, jobId);
×
697
        }
698

699
        void setJobResources(int jobId, ResourceUsage resources) {
700
                var wrapper = new JobResourceUpdate();
×
701
                wrapper.setResourceUsage(resources);
×
702
                proxy.setJobResources(apiKey, jobId, wrapper);
×
703
        }
×
704

705
        List<Project> getProjects(String status, String collab) {
706
                return proxy.getProjects(apiKey, status, collab);
×
707
        }
708

709
        SessionResponse createSession(String collab, String user) {
710
                var request = new SessionRequest();
×
711
                request.setCollab(collab);
×
712
                request.setHardwarePlatform(platform);
×
713
                request.setUserId(user);
×
714
                return proxy.createSession(apiKey, request);
×
715
        }
716

717
        void setSessionStatusAndResources(int sessionId, String status,
718
                        ResourceUsage resources) {
719
                var wrapper = new SessionResourceUpdate();
×
720
                wrapper.setStatus(status);
×
721
                wrapper.setResourceUsage(resources);
×
722
                proxy.setSessionStatusAndResources(apiKey, sessionId, wrapper);
×
723
        }
×
724
}
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