Skip to content

Commit 148a8a1

Browse files
pditommasoclaudebentsherman
authored
Refactor Wave and Tower client to use lib-httpx library (#6354)
* Refactor Wave client to use lib-httpx library - Replace manual HTTP retry logic with HxClient from io.seqera:lib-httpx:1.2.0 - Update RetryOpts to implement Retryable.Config interface from lib-retry - Add multiplier field to RetryOpts for exponential backoff configuration - Remove manual JWT token refresh logic in favor of HxClient built-in handling - Update WaveClient to use HxClient.sendAsString() instead of raw HttpClient - Remove redundant retry policy and HTTP response handling code - Update tests to reflect API changes and remove obsolete HTTP retry test 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Bump httpx@1.3.0 Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com> * Improve integration Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com> * Refactor Tower client to use lib-httpx library (#6357) * Refactor Tower client to use lib-httpx library - Replace manual HTTP retry logic with HxClient from io.seqera:lib-httpx - Update TowerClient.sendHttpMessage() to use HxClient.sendAsString() - Add HxClient configuration with retry settings, JWT token, and refresh token handling - Update makeRequest() helper method to properly build HttpRequest objects with body content - Remove redundant HTTP response handling code in favor of HxClient built-in functionality - Update SimpleHttpClient to support content-type and authorization headers - Update tests to reflect API changes - Add comprehensive annotations to TowerRetryPolicy with default values - Add Javadoc documentation to TowerRetryPolicy class explaining retry behavior - Fix missing annotations in WaveConfig BuildOpts class (conda, compression, maxDuration) Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com> Signed-off-by: Ben Sherman <bentshermann@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ben Sherman <bentshermann@gmail.com>
1 parent a3db0f8 commit 148a8a1

File tree

21 files changed

+481
-313
lines changed

21 files changed

+481
-313
lines changed

modules/nextflow/src/main/groovy/nextflow/util/SimpleHttpClient.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import nextflow.BuildInfo
3232
* Paolo Di Tommaso <paolo.ditommaso@gmail.com>
3333
*/
3434
@Slf4j
35+
@Deprecated
3536
@CompileStatic
3637
class SimpleHttpClient {
3738

modules/nf-commons/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ dependencies {
3434
api 'org.pf4j:pf4j:3.12.0'
3535
api 'org.pf4j:pf4j-update:2.3.0'
3636
api 'dev.failsafe:failsafe:3.1.0'
37-
api 'io.seqera:lib-httpx:1.0.0'
38-
api 'io.seqera:lib-retry:1.2.0'
37+
api 'io.seqera:lib-httpx:2.0.0'
38+
api 'io.seqera:lib-retry:2.0.0'
3939
// patch gson dependency required by pf4j
4040
api 'com.google.code.gson:gson:2.13.1'
4141
api 'io.seqera:npr-api:0.6.1'

modules/nf-commons/src/main/nextflow/plugin/HttpPluginRepository.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ class HttpPluginRepository implements PrefetchUpdateRepository {
4444
this.url = !url.toString().endsWith("/")
4545
? URI.create(url.toString() + "/")
4646
: url
47-
this.httpClient = HxClient.create(RetryConfig.config())
47+
this.httpClient = HxClient.newBuilder()
48+
.retryConfig(RetryConfig.config())
49+
.build()
4850
}
4951

5052
// NOTE ON PREFETCHING
@@ -135,7 +137,7 @@ class HttpPluginRepository implements PrefetchUpdateRepository {
135137
throw e
136138
}
137139
catch (Exception e) {
138-
throw new PluginRuntimeException(e, "Unable to connect to ${uri}- cause: ${e.message}")
140+
throw new PluginRuntimeException(e, "Unable to connect to ${uri} - cause: ${e.message}")
139141
}
140142
}
141143

modules/nf-commons/src/main/nextflow/util/Duration.groovy

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package nextflow.util
1818

19+
import java.time.temporal.ChronoUnit
1920
import java.time.temporal.Temporal
21+
import java.time.temporal.TemporalAmount
22+
import java.time.temporal.TemporalUnit
23+
import java.time.temporal.UnsupportedTemporalTypeException
2024
import java.util.concurrent.TimeUnit
2125

2226
import groovy.transform.CompileStatic
@@ -32,7 +36,9 @@ import org.apache.commons.lang.time.DurationFormatUtils
3236
@Slf4j
3337
@CompileStatic
3438
@EqualsAndHashCode(includes = 'durationInMillis')
35-
class Duration implements IDuration, Comparable<Duration>, Serializable, Cloneable {
39+
class Duration implements IDuration, TemporalAmount, Comparable<Duration>, Serializable, Cloneable {
40+
41+
static private final List<TemporalUnit> SUPPORTED_UNITS = List.<TemporalUnit>of(ChronoUnit.DAYS, ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS, ChronoUnit.MILLIS)
3642

3743
static private final FORMAT = ~/^(\d+\.?\d*)\s*([a-zA-Z]+)/
3844

@@ -365,4 +371,64 @@ class Duration implements IDuration, Comparable<Duration>, Serializable, Cloneab
365371
throw new IllegalArgumentException("Not a valid duration value: $right")
366372
}
367373

374+
// TemporalAmount interface methods
375+
376+
/**
377+
* Gets the value of the requested unit.
378+
*
379+
* @param unit the TemporalUnit for which to return the value
380+
* @return the long value of the unit
381+
*/
382+
@Override
383+
long get(TemporalUnit unit) {
384+
if (unit == ChronoUnit.MILLIS) {
385+
return durationInMillis
386+
}
387+
if (unit == ChronoUnit.SECONDS) {
388+
return toSeconds()
389+
}
390+
if (unit == ChronoUnit.MINUTES) {
391+
return toMinutes()
392+
}
393+
if (unit == ChronoUnit.HOURS) {
394+
return toHours()
395+
}
396+
if (unit == ChronoUnit.DAYS) {
397+
return toDays()
398+
}
399+
throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit)
400+
}
401+
402+
/**
403+
* Returns a list of units uniquely defining the value of this TemporalAmount.
404+
*
405+
* @return a list of the supported ChronoUnits, not null
406+
*/
407+
@Override
408+
List<TemporalUnit> getUnits() {
409+
return SUPPORTED_UNITS
410+
}
411+
412+
/**
413+
* Adds this amount to the specified temporal object.
414+
*
415+
* @param temporal the temporal object to adjust, not null
416+
* @return an object of the same type with the adjustment made, not null
417+
*/
418+
@Override
419+
Temporal addTo(Temporal temporal) {
420+
return temporal.plus(durationInMillis, ChronoUnit.MILLIS)
421+
}
422+
423+
/**
424+
* Subtracts this amount from the specified temporal object.
425+
*
426+
* @param temporal the temporal object to adjust, not null
427+
* @return an object of the same type with the adjustment made, not null
428+
*/
429+
@Override
430+
Temporal subtractFrom(Temporal temporal) {
431+
return temporal.minus(durationInMillis, ChronoUnit.MILLIS)
432+
}
433+
368434
}

modules/nf-commons/src/main/nextflow/util/RetryConfig.groovy

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ import nextflow.SysEnv
3737
@CompileStatic
3838
class RetryConfig implements Retryable.Config {
3939

40-
private final static Duration DEFAULT_DELAY = Duration.of('350ms')
41-
private final static Duration DEFAULT_MAX_DELAY = Duration.of('90s')
42-
private final static Integer DEFAULT_MAX_ATTEMPTS = 5
43-
private final static Double DEFAULT_JITTER = 0.25
44-
static final public double DEFAULT_MULTIPLIER = 2.0
40+
public static final Duration DEFAULT_DELAY = Duration.of('350ms')
41+
public static final Duration DEFAULT_MAX_DELAY = Duration.of('90s')
42+
public static final Integer DEFAULT_MAX_ATTEMPTS = 5
43+
public static final Double DEFAULT_JITTER = 0.25
44+
public static final double DEFAULT_MULTIPLIER = 2.0
4545

4646
private final static String ENV_PREFIX = 'NXF_RETRY_POLICY_'
4747

@@ -68,17 +68,18 @@ class RetryConfig implements Retryable.Config {
6868
valueOf(config, 'multiplier', ENV_PREFIX, DEFAULT_MULTIPLIER, Double)
6969
}
7070

71-
java.time.Duration getDelay() {
72-
return java.time.Duration.ofMillis(delay.toMillis())
71+
Duration getDelay() {
72+
return delay
7373
}
7474

75-
java.time.Duration getMaxDelay() {
76-
return java.time.Duration.ofMillis(maxDelay.toMillis())
75+
Duration getMaxDelay() {
76+
return maxDelay
7777
}
7878

7979
@Override
8080
int getMaxAttempts() {
81-
return maxAttempts }
81+
return maxAttempts
82+
}
8283

8384
@Override
8485
double getJitter() {

modules/nf-commons/src/test/nextflow/util/DurationTest.groovy

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package nextflow.util
1818

1919
import java.time.Instant
20+
import java.time.LocalDateTime
21+
import java.time.temporal.ChronoUnit
22+
import java.time.temporal.UnsupportedTemporalTypeException
2023

2124
import spock.lang.Specification
2225
import spock.lang.Unroll
@@ -196,5 +199,97 @@ class DurationTest extends Specification {
196199
Duration.between(start, end) == Duration.of('1sec')
197200
}
198201

202+
// TemporalAmount interface tests
203+
204+
def 'should get value for supported temporal units' () {
205+
given:
206+
def duration = new Duration('2d 3h 4m 5s 123ms')
207+
208+
expect:
209+
duration.get(ChronoUnit.MILLIS) == duration.toMillis()
210+
duration.get(ChronoUnit.SECONDS) == duration.toSeconds()
211+
duration.get(ChronoUnit.MINUTES) == duration.toMinutes()
212+
duration.get(ChronoUnit.HOURS) == duration.toHours()
213+
duration.get(ChronoUnit.DAYS) == duration.toDays()
214+
}
215+
216+
def 'should throw exception for unsupported temporal unit' () {
217+
given:
218+
def duration = new Duration('1h')
219+
220+
when:
221+
duration.get(ChronoUnit.WEEKS)
222+
223+
then:
224+
thrown(UnsupportedTemporalTypeException)
225+
}
226+
227+
def 'should return list of supported units' () {
228+
given:
229+
def duration = new Duration('1h')
230+
231+
when:
232+
def units = duration.getUnits()
233+
234+
then:
235+
units.size() == 5
236+
units.contains(ChronoUnit.DAYS)
237+
units.contains(ChronoUnit.HOURS)
238+
units.contains(ChronoUnit.MINUTES)
239+
units.contains(ChronoUnit.SECONDS)
240+
units.contains(ChronoUnit.MILLIS)
241+
// Should be ordered from longest to shortest
242+
units[0] == ChronoUnit.DAYS
243+
units[4] == ChronoUnit.MILLIS
244+
}
245+
246+
def 'should add duration to temporal' () {
247+
given:
248+
def duration = new Duration('2h 30m')
249+
def dateTime = LocalDateTime.of(2023, 1, 1, 10, 0, 0)
250+
251+
when:
252+
def result = duration.addTo(dateTime)
253+
254+
then:
255+
result == LocalDateTime.of(2023, 1, 1, 12, 30, 0)
256+
}
257+
258+
def 'should subtract duration from temporal' () {
259+
given:
260+
def duration = new Duration('1h 15m')
261+
def dateTime = LocalDateTime.of(2023, 1, 1, 12, 30, 0)
262+
263+
when:
264+
def result = duration.subtractFrom(dateTime)
265+
266+
then:
267+
result == LocalDateTime.of(2023, 1, 1, 11, 15, 0)
268+
}
269+
270+
def 'should add duration to instant' () {
271+
given:
272+
def duration = new Duration('5s')
273+
def instant = Instant.ofEpochMilli(1000)
274+
275+
when:
276+
def result = duration.addTo(instant)
277+
278+
then:
279+
result == Instant.ofEpochMilli(6000)
280+
}
281+
282+
def 'should subtract duration from instant' () {
283+
given:
284+
def duration = new Duration('2s')
285+
def instant = Instant.ofEpochMilli(5000)
286+
287+
when:
288+
def result = duration.subtractFrom(instant)
289+
290+
then:
291+
result == Instant.ofEpochMilli(3000)
292+
}
293+
199294

200295
}

modules/nf-commons/src/test/nextflow/util/RetryConfigTest.groovy

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ class RetryConfigTest extends Specification {
2828

2929
def 'should create retry config' () {
3030
expect:
31-
new RetryConfig().delay == java.time.Duration.ofMillis(350)
32-
new RetryConfig().maxDelay == java.time.Duration.ofSeconds(90)
31+
new RetryConfig().delay == Duration.of('350ms')
32+
new RetryConfig().maxDelay == Duration.of('90s')
3333
new RetryConfig().maxAttempts == 5
3434
new RetryConfig().jitter == 0.25d
3535
new RetryConfig().multiplier == 2d
3636

3737
and:
3838
new RetryConfig([maxAttempts: 20]).maxAttempts == 20
39-
new RetryConfig([delay: '1s']).delay == java.time.Duration.ofSeconds(1)
40-
new RetryConfig([maxDelay: '1m']).maxDelay == java.time.Duration.ofMinutes(1)
39+
new RetryConfig([delay: '1s']).delay == Duration.of('1s')
40+
new RetryConfig([maxDelay: '1m']).maxDelay == Duration.of('1m')
4141
new RetryConfig([jitter: '0.5']).jitter == 0.5d
4242
new RetryConfig([multiplier: '5']).multiplier == 5d
4343
}
@@ -53,8 +53,8 @@ class RetryConfigTest extends Specification {
5353
])
5454

5555
expect:
56-
new RetryConfig().getDelay() == java.time.Duration.ofSeconds(10)
57-
new RetryConfig().getMaxDelay() == java.time.Duration.ofSeconds(100)
56+
new RetryConfig().getDelay() == Duration.of('10s')
57+
new RetryConfig().getMaxDelay() == Duration.of('100s')
5858
new RetryConfig().getMaxAttempts() == 1000
5959
new RetryConfig().getJitter() == 10_000
6060
new RetryConfig().getMultiplier() == 90d

plugins/nf-tower/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
compileOnly 'org.slf4j:slf4j-api:2.0.17'
3434
compileOnly 'org.pf4j:pf4j:3.12.0'
3535

36+
api 'io.seqera:lib-httpx:2.0.0'
3637
api "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.0"
3738
api "com.fasterxml.jackson.core:jackson-databind:2.12.7.1"
3839

0 commit comments

Comments
 (0)