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

SpiNNakerManchester / JavaSpiNNaker / 7248

11 Nov 2025 09:01AM UTC coverage: 36.234% (-0.02%) from 36.254%
7248

push

github

web-flow
Merge pull request #1353 from SpiNNakerManchester/dependabot/maven/com.mysql-mysql-connector-j-9.5.0

Bump com.mysql:mysql-connector-j from 9.4.0 to 9.5.0

1910 of 5898 branches covered (32.38%)

Branch coverage included in aggregate %.

8961 of 24104 relevant lines covered (37.18%)

0.74 hits per line

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

23.55
/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 jakarta.annotation.PostConstruct;
31
import jakarta.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.AuthProperties;
44
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.QuotaProperties;
45
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Connection;
46
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Query;
47
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Update;
48
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
49
import uk.ac.manchester.spinnaker.alloc.db.Row;
50
import uk.ac.manchester.spinnaker.alloc.nmpi.Job;
51
import uk.ac.manchester.spinnaker.alloc.nmpi.JobResourceUpdate;
52
import uk.ac.manchester.spinnaker.alloc.nmpi.NMPIv3API;
53
import uk.ac.manchester.spinnaker.alloc.nmpi.Project;
54
import uk.ac.manchester.spinnaker.alloc.nmpi.ResourceUsage;
55
import uk.ac.manchester.spinnaker.alloc.nmpi.SessionRequest;
56
import uk.ac.manchester.spinnaker.alloc.nmpi.SessionResourceUpdate;
57
import uk.ac.manchester.spinnaker.alloc.nmpi.SessionResponse;
58

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

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

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

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

83
        @Autowired
84
        private ServiceMasterControl control;
85

86
        @Autowired
87
        private QuotaProperties quotaProps;
88

89
        @Autowired
90
        private AuthProperties authProps;
91

92
        /** The wrapped NMPI proxy, bound to the API key. */
93
        private NMPI nmpi;
94

95
        /**
96
         * Make the NMPI access interface.
97
         */
98
        @PostConstruct
99
        private void createProxy() {
100
                var nmpiUrl = quotaProps.getNMPIUrl();
2✔
101
                if (!nmpiUrl.isEmpty()) {
2!
102
                        nmpi = new NMPI(quotaProps);
×
103
                }
104
        }
2✔
105

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

120
        private final class CreateCheckSQL extends AbstractSQL {
2✔
121
                // TODO These should be combined (but one is an aggregate so...)
122
                private final Query getQuota = conn.query(GET_GROUP_QUOTA);
2✔
123

124
                private final Query getCurrentUsage = conn.query(GET_CURRENT_USAGE);
2✔
125

126
                @Override
127
                public void close() {
128
                        getQuota.close();
2✔
129
                        getCurrentUsage.close();
2✔
130
                        super.close();
2✔
131
                }
2✔
132

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

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

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

176
        private final class ContinueCheckSQL extends AbstractSQL {
×
177
                private final Query getUsageAndQuota =
×
178
                                conn.query(GET_JOB_USAGE_AND_QUOTA);
×
179

180
                @Override
181
                public void close() {
182
                        getUsageAndQuota.close();
×
183
                        super.close();
×
184
                }
×
185

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

192
                                        // Otherwise, we'll just allow it
193
                                        .orElse(true);
×
194
                }
195
        }
196

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

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

222
                private final Long quota;
223

224
                private AdjustedQuota(Row row) {
×
225
                        this.name = row.getString("group_name");
×
226
                        this.quota = row.getLong("quota");
×
227
                }
×
228

229
                /** @return The name of the group. */
230
                public String getName() {
231
                        return name;
×
232
                }
233

234
                /** @return The new quota of the group. */
235
                public Long getQuota() {
236
                        return quota;
×
237
                }
238
        }
239

240
        private final class AdjustQuotaSQL extends AbstractSQL {
×
241
                private final Update adjustQuota = conn.update(ADJUST_QUOTA);
×
242

243
                private final Query getQuota = conn.query(GET_GROUP_QUOTA);
×
244

245
                @Override
246
                public void close() {
247
                        adjustQuota.close();
×
248
                        super.close();
×
249
                }
×
250

251
                private Optional<AdjustedQuota> adjustQuota(int groupId, int delta) {
252
                        if (adjustQuota.call(delta, groupId) == 0) {
×
253
                                return Optional.empty();
×
254
                        }
255
                        return getQuota.call1(AdjustedQuota::new, groupId);
×
256
                }
257
        }
258

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

280
        private void doConsolidate(Connection c) {
281
                try (var sql = new ConsolidateSQL(c)) {
2✔
282
                        sql.transaction(sql::consolidate);
2✔
283
                }
284
        }
2✔
285

286
        private class ConsolidateSQL extends AbstractSQL {
287
                private final Query getConsoldationTargets =
2✔
288
                                conn.query(GET_CONSOLIDATION_TARGETS);
2✔
289

290
                private final Update decrementQuota = conn.update(DECREMENT_QUOTA);
2✔
291

292
                private final Update markConsolidated = conn.update(MARK_CONSOLIDATED);
2✔
293

294
                ConsolidateSQL(Connection c) {
2✔
295
                        super(c);
2✔
296
                }
2✔
297

298
                @Override
299
                public void close() {
300
                        getConsoldationTargets.close();
2✔
301
                        decrementQuota.close();
2✔
302
                        markConsolidated.close();
2✔
303
                        super.close();
2✔
304
                }
2✔
305

306
                // Result is arbitrary and ignored
307
                private Void consolidate() {
308
                        for (var target : getConsoldationTargets.call(Target::new)) {
2✔
309
                                decrementQuota.call(target.quotaUsed, target.groupId);
2✔
310
                                markConsolidated.call(target.jobId);
2✔
311
                        }
2✔
312
                        return null;
2✔
313
                }
314

315
                private class Target {
316
                        private final Object quotaUsed;
317

318
                        private final int groupId;
319

320
                        private final int jobId;
321

322
                        Target(Row row) {
2✔
323
                                quotaUsed = row.getObject("quota_used");
2✔
324
                                groupId = row.getInt("group_id");
2✔
325
                                jobId = row.getInt("job_id");
2✔
326
                        }
2✔
327
                }
328
        }
329

330
        private static class QuotaInfo {
331
                /** The size of quota remaining, in board-seconds. */
332
                final long quota;
333

334
                /** The units that the quota was measured in on the NMPI. */
335
                final String units;
336

337
                QuotaInfo(long quota, String units) {
×
338
                        this.quota = quota;
×
339
                        this.units = units;
×
340
                }
×
341
        }
342

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

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

379
        final Optional<String> checkQuota(String collab) {
380
                // Read collab from NMPI; fail if not there
381
                var projects = nmpi.getProjects(STATUS_ACCEPTED, collab);
×
382
                var info = parseQuotaData(projects);
×
383

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

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

392
                if (info.quota > 0) {
×
393
                        return Optional.of(info.units);
×
394
                }
395
                return Optional.empty();
×
396
        }
397

398
        private String getNMPIUser(String user) {
399
                var oidPrefix = authProps.getOpenid().getUsernamePrefix();
×
400
                if (user.startsWith(oidPrefix)) {
×
401
                        return user.substring(oidPrefix.length());
×
402
                }
403
                return user;
×
404
        }
405

406
        final SessionResponse createSession(String collab, String user) {
407
                var session = nmpi.createSession(collab, getNMPIUser(user));
×
408
                checkQuota(collab);
×
409
                return session;
×
410
        }
411

412
        void associateNMPISession(int jobId, int sessionId, String quotaUnits) {
413
                // Associate NMPI session with Job in the database
414
                try (var c = getConnection();
×
415
                                var setSession = c.update(SET_JOB_SESSION)) {
×
416
                        c.transaction(
×
417
                                        () -> setSession.call(jobId, sessionId, quotaUnits));
×
418
                }
419
        }
×
420

421
        private final class InflateUser extends AbstractSQL {
×
422
                private final Query getUserByName =
×
423
                                conn.query(GET_USER_DETAILS_BY_NAME);
×
424

425
                private final Query getGroupByName = conn.query(GET_GROUP_BY_NAME);
×
426

427
                private final Update createUser = conn.update(CREATE_USER);
×
428

429
                private final Update createGroup =
×
430
                                conn.update(CREATE_GROUP_IF_NOT_EXISTS);
×
431

432
                private final Update addUserToGroup = conn.update(ADD_USER_TO_GROUP);
×
433

434
                @Override
435
                public void close() {
436
                        getUserByName.close();
×
437
                        getGroupByName.close();
×
438
                        createUser.close();
×
439
                        createGroup.close();
×
440
                        addUserToGroup.close();
×
441
                        super.close();
×
442
                }
×
443

444
                private Optional<Integer> getUserByName(String user) {
445
                        return getUserByName.call1(r -> r.getInt("user_id"), user);
×
446
                }
447

448
                private Optional<Integer> getGroupByName(String group) {
449
                        return getGroupByName.call1(r -> r.getInt("group_id"), group);
×
450
                }
451

452
                private boolean createUser(String user) {
453
                        return createUser.call(user, null, USER, false, null) > 0;
×
454
                }
455

456
                private boolean createGroup(String group) {
457
                        return createGroup.call(group, 0.0, COLLABRATORY) > 0;
×
458
                }
459

460
                private boolean addUserToGroup(int userId, Optional<Integer> groupId) {
461
                        return addUserToGroup.call(userId, groupId) > 0;
×
462
                }
463

464
                /**
465
                 * Construct the user and group records if necessary and associate them.
466
                 *
467
                 * @param user
468
                 *            The user name.
469
                 * @param job
470
                 *            The NMPI job details containing the collabratory (group)
471
                 *            name.
472
                 */
473
                void inflateUser(String user, Job job) {
474
                        var userId = getUserByName(user).or(() -> {
×
475
                                /*
476
                                 * The user has never logged in directly, so we have to create
477
                                 * them.
478
                                 */
479
                                if (!createUser(user)) {
×
480
                                        log.warn("failed to make user: {}", user);
×
481
                                }
482
                                return getUserByName(user);
×
483
                        });
484

485
                        createGroup(job.getCollab());
×
486
                        var groupId = getGroupByName(job.getCollab());
×
487
                        addUserToGroup(userId.orElseThrow(() -> new IllegalStateException(
×
488
                                        "failed to find or create user")), groupId);
489
                }
×
490
        }
491

492
        final Optional<NMPIJobQuotaDetails> mayUseNMPIJob(String user,
493
                        int nmpiJobId) {
494
                // Read job from NMPI to get collab ID
495
                var job = nmpi.getJob(nmpiJobId);
×
496

497
                // If it is possible to run this job, we need to associate the user
498
                // with it because only special users can run jobs like this.
499
                try (var sql = new InflateUser()) {
×
500
                        sql.transaction(() -> sql.inflateUser(user, job));
×
501
                }
502

503
                // This is now a collab so check there instead
504
                var quotaUnits = checkQuota(job.getCollab());
×
505
                if (quotaUnits.isEmpty()) {
×
506
                        return Optional.empty();
×
507
                }
508

509
                return Optional.of(
×
510
                                new NMPIJobQuotaDetails(job.getCollab(), quotaUnits.get()));
×
511
        }
512

513
        static final class NMPIJobQuotaDetails {
514
                /** The collaboratory ID. */
515
                final String collabId;
516

517
                /** The units of the Quota. */
518
                final String quotaUnits;
519

520
                private NMPIJobQuotaDetails(String collabId, String quotaUnits) {
×
521
                        this.collabId = collabId;
×
522
                        this.quotaUnits = quotaUnits;
×
523
                }
×
524
        }
525

526
        void associateNMPIJob(int jobId, int nmpiJobId, String quotaUnits) {
527
                // Associate NMPI Job with job in database
528
                try (var c = getConnection();
×
529
                                var setNMPIJob = c.update(SET_JOB_NMPI_JOB)) {
×
530
                        c.transaction(() -> setNMPIJob.call(jobId, nmpiJobId, quotaUnits));
×
531
                }
532
        }
×
533

534
        /** Results of database queries. */
535
        private static final class FinishInfo {
536
                private Optional<Long> quota;
537

538
                private Optional<Session> session;
539

540
                private Optional<NMPIJob> job;
541
        }
542

543
        private FinishInfo getFinishingInfo(int jobId) {
544
                try (var c = getConnection();
2✔
545
                                var getSession = c.query(GET_JOB_SESSION);
2✔
546
                                var getNMPIJob = c.query(GET_JOB_NMPI_JOB);
2✔
547
                                var getUsage = c.query(GET_JOB_USAGE_AND_QUOTA)) {
2✔
548
                        // Get the quota used
549
                        return c.transaction(false, () -> {
2✔
550
                                var i = new FinishInfo();
2✔
551
                                i.quota = getUsage.call1(r -> r.getLong("quota_used"), jobId);
2✔
552
                                i.session = getSession.call1(Session::new, jobId);
2✔
553
                                i.job = getNMPIJob.call1(NMPIJob::new, jobId);
2✔
554
                                return i;
2✔
555
                        });
556
                }
557
        }
558

559
        final void finishJob(int jobId) {
560
                // Get the information about the job from the DB
561
                var info = getFinishingInfo(jobId);
2✔
562

563
                // From here on, we don't touch the DB but we do touch the network
564

565
                if (!info.quota.isPresent()) {
2!
566
                        // No quota? No update!
567
                        return;
×
568
                }
569

570
                // If job has associated session, update quota in session
571
                info.session.ifPresent(session -> {
2✔
572
                        try {
573
                                nmpi.setSessionStatusAndResources(session.id, "finished",
×
574
                                                getResourceUsage(info.quota.get(), session.quotaUnits));
×
575
                        } catch (BadRequestException e) {
×
576
                                log.error(e.getResponse().readEntity(String.class));
×
577
                                throw e;
×
578
                        }
×
579
                });
×
580

581
                // If job has associated NMPI job, update quota on NMPI job
582
                info.job.ifPresent(nmpiJob -> {
2✔
583
                        try {
584
                                nmpi.setJobResources(nmpiJob.id,
×
585
                                                getResourceUsage(info.quota.get(), nmpiJob.quotaUnits));
×
586
                        } catch (BadRequestException e) {
×
587
                                log.error(e.getResponse().readEntity(String.class));
×
588
                                throw e;
×
589
                        }
×
590
                });
×
591
        }
2✔
592

593
        private static final class Session {
594
                private int id;
595

596
                private String quotaUnits;
597

598
                private Session(Row r) {
×
599
                        this.id = r.getInt("session_id");
×
600
                        this.quotaUnits = r.getString("quota_units");
×
601
                }
×
602
        }
603

604
        private static final class NMPIJob {
605
                private int id;
606

607
                private String quotaUnits;
608

609
                private NMPIJob(Row r) {
×
610
                        this.id = r.getInt("nmpi_job_id");
×
611
                        this.quotaUnits = r.getString("quota_units");
×
612
                }
×
613
        }
614

615
        private static ResourceUsage getResourceUsage(long boardSeconds,
616
                        String units) {
617
                var resourceUsage = new ResourceUsage();
×
618
                resourceUsage.setUnits(units);
×
619
                if (units.equals(BOARD_SECONDS)) {
×
620
                        resourceUsage.setValue(boardSeconds);
×
621
                } else if (units.equals(CORE_HOURS)) {
×
622
                        resourceUsage.setValue(toCoreHours(boardSeconds));
×
623
                } else {
624
                        throw new IllegalArgumentException("Unknown units " + units);
×
625
                }
626
                return resourceUsage;
×
627
        }
628

629
        /**
630
         * Convert board-seconds to core-hours (approximately).
631
         *
632
         * @param boardSeconds
633
         *            The number of board-seconds to convert.
634
         * @return The number of board-hours, which may have fractional values.
635
         */
636
        private static double toCoreHours(long boardSeconds) {
637
                return ((double) (boardSeconds * APPROX_CORES_PER_BOARD))
×
638
                                / SECONDS_PER_HOUR;
639
        }
640

641
        /**
642
         * Convert core-hours to board-seconds (approximately).
643
         *
644
         * @param coreHours
645
         *            The number of core-hours to convert.
646
         * @return The integer number of board seconds.
647
         */
648
        private static long toBoardSeconds(double coreHours) {
649
                return (long) ((coreHours * SECONDS_PER_HOUR) / APPROX_CORES_PER_BOARD);
×
650
        }
651

652
        /**
653
         * Operations for testing only.
654
         *
655
         * @hidden
656
         */
657
        @ForTestingOnly
658
        interface TestAPI {
659
                /**
660
                 * Consolidates usage from finished jobs onto quotas.
661
                 *
662
                 * @param c
663
                 *            How to talk to the DB.
664
                 */
665
                void doConsolidate(Connection c);
666
        }
667

668
        /**
669
         * @return The test interface.
670
         * @deprecated This interface is just for testing.
671
         * @hidden
672
         */
673
        @ForTestingOnly
674
        @RestrictedApi(explanation = "just for testing", link = "index.html",
675
                        allowedOnPath = ".*/src/test/java/.*")
676
        @Deprecated
677
        TestAPI getTestAPI() {
678
                ForTestingOnly.Utils.checkForTestClassOnStack();
2✔
679
                return new TestAPI() {
2✔
680
                        @Override
681
                        public void doConsolidate(Connection c) {
682
                                QuotaManager.this.doConsolidate(c);
2✔
683
                        }
2✔
684
                };
685
        }
686
}
687

688
/**
689
 * Wrapper round the proxy and its API key. Does a bit of wrapping and
690
 * unwrapping of arguments and results.
691
 *
692
 * @author Donal Fellows
693
 */
694
final class NMPI {
695
        private final NMPIv3API proxy;
696

697
        private final String apiKey;
698

699
        private final String platform;
700

701
        NMPI(QuotaProperties quotaProps) {
×
702
                proxy = NMPIv3API.createClient(quotaProps.getNMPIUrl());
×
703
                apiKey = quotaProps.getNMPIApiKey();
×
704
                platform = quotaProps.getNMPIPlaform();
×
705
        }
×
706

707
        Job getJob(int jobId) {
708
                return proxy.getJob(apiKey, jobId);
×
709
        }
710

711
        void setJobResources(int jobId, ResourceUsage resources) {
712
                var wrapper = new JobResourceUpdate();
×
713
                wrapper.setResourceUsage(resources);
×
714
                proxy.setJobResources(apiKey, jobId, wrapper);
×
715
        }
×
716

717
        List<Project> getProjects(String status, String collab) {
718
                return proxy.getProjects(apiKey, status, collab);
×
719
        }
720

721
        SessionResponse createSession(String collab, String user) {
722
                var request = new SessionRequest();
×
723
                request.setCollab(collab);
×
724
                request.setHardwarePlatform(platform);
×
725
                request.setUserId(user);
×
726
                return proxy.createSession(apiKey, request);
×
727
        }
728

729
        void setSessionStatusAndResources(int sessionId, String status,
730
                        ResourceUsage resources) {
731
                var wrapper = new SessionResourceUpdate();
×
732
                wrapper.setStatus(status);
×
733
                wrapper.setResourceUsage(resources);
×
734
                proxy.setSessionStatusAndResources(apiKey, sessionId, wrapper);
×
735
        }
×
736
}
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