Skip to content

Commit 5096111

Browse files
committed
Merge branch 'develop': release 1.4
2 parents 245b584 + 0abe311 commit 5096111

File tree

6 files changed

+254
-15
lines changed

6 files changed

+254
-15
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ kinfra-commons
3939

4040
Аналог `Instant` на основе `System.nanoTime()` для измерения прошедшего времени.
4141

42+
#### Класс Deadline
43+
44+
Предназначен для передачи в вызываемый код информации о том, когда истечёт таймаут его выполнения.
45+
46+
Для корректной работы этого механизма необходимо использовать функцию `withDeadlineAfter()` вместо `withTimeout()`.
47+
48+
Пример использования:
49+
50+
withDeadlineAfter(Duration.ofSeconds(10)) {
51+
....
52+
println("Time left: " + coroutineContext[Deadline]?.timeLeft())
53+
}
54+
4255
#### Класс TimeTicks
4356

4457
Представляет время в тиках (100 наносекунд)
@@ -78,10 +91,9 @@ kinfra-commons
7891
* `ByteArray.toHexString()` и `byteArrayOfHex()`
7992
* `StringBuilder.appendHexByte()`
8093

81-
### Either
94+
### Either<L, R>
8295

83-
Библиотека предоставляет класс Either для представления выбора между ошибочным состоянием
84-
и успешным получением результата.
96+
Представляет выбор между ошибочным состоянием (L) и успешным получением результата (R).
8597

8698
Сборка
8799
------

build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ plugins {
44
}
55

66
ext {
7-
release = '1.3.1'
7+
release = '1.4'
88

99
versions = [
1010
assertj: '3.14.0',
11+
coroutines: '1.3.3',
1112
]
1213

1314
branch = project.properties["branch"]?.toString()
@@ -21,5 +22,9 @@ version = ext.version
2122
sourceCompatibility = 8
2223

2324
dependencies {
25+
allMain platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:${versions.coroutines}")
26+
27+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8"
28+
2429
testImplementation "org.assertj:assertj-core:${versions.assertj}"
2530
}

src/main/kotlin/ru/kontur/kinfra/commons/Either.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ sealed class Either<out L, out R> {
3030

3131
fun <L> left(error: L): Either<L, Nothing> = Left(error)
3232

33-
@Deprecated(level = DeprecationLevel.ERROR, message = "Don't use Either for exceptions")
34-
fun <E> left(error: E): Either<E, Nothing> where E : Throwable = Left(error)
35-
3633
fun <R> right(value: R): Either<Nothing, R> = Right(value)
3734

3835
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package ru.kontur.kinfra.commons.time
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.TimeoutCancellationException
5+
import kotlinx.coroutines.time.withTimeout
6+
import kotlinx.coroutines.withContext
7+
import ru.kontur.kinfra.commons.time.MonotonicInstant.Companion.now
8+
import java.time.Duration
9+
import java.time.temporal.ChronoUnit
10+
import kotlin.coroutines.CoroutineContext
11+
import kotlin.coroutines.coroutineContext
12+
13+
/**
14+
* Represents a moment of time when a coroutine's timeout will expire.
15+
*
16+
* Purpose of this class is to inform a code running in a coroutine on the time when it should complete its execution.
17+
*
18+
* For this to work [withDeadlineAfter] should be used instead of [withTimeout] across the entire codebase.
19+
*
20+
* **Warning**: don't use `withContext(deadline)`, as it does not setup timeout
21+
* and unconditionally replaces current deadline with a given one.
22+
* Always use `withDeadline()` instead.
23+
*
24+
* Usage:
25+
*
26+
* 1. Run with timeout:
27+
* ```
28+
* withDeadlineAfter(Duration.ofSeconds(10)) {
29+
* ....
30+
* println("Time left: " + coroutineContext[Deadline]?.timeLeft())
31+
* }
32+
* ```
33+
*
34+
* 2. Run with a specified deadline:
35+
* ```
36+
* val deadline = ...
37+
* withDeadline(deadline) {
38+
* ...
39+
* }
40+
* ```
41+
*/
42+
class Deadline private constructor(
43+
private val time: MonotonicInstant
44+
) : CoroutineContext.Element, Comparable<Deadline> {
45+
46+
fun isPassed(): Boolean = timeLeft() <= Duration.ZERO
47+
48+
/**
49+
* Returns an amount of time before this deadline will [pass][isPassed].
50+
*
51+
* Negative values represent the time elapsed since this deadline is passed.
52+
*/
53+
fun timeLeft(): Duration {
54+
// Result is truncated because withTimeout's precision is a millisecond
55+
return (time - now()).truncatedTo(ChronoUnit.MILLIS)
56+
}
57+
58+
operator fun plus(offset: Duration) = Deadline(time + offset)
59+
60+
operator fun minus(offset: Duration) = Deadline(time - offset)
61+
62+
override fun compareTo(other: Deadline): Int {
63+
return time.compareTo(other.time)
64+
}
65+
66+
override val key: CoroutineContext.Key<*>
67+
get() = Deadline
68+
69+
override fun equals(other: Any?): Boolean {
70+
return other is Deadline && other.time == this.time
71+
}
72+
73+
override fun hashCode(): Int {
74+
return time.hashCode()
75+
}
76+
77+
override fun toString(): String {
78+
return "Deadline($time)"
79+
}
80+
81+
companion object : CoroutineContext.Key<Deadline> {
82+
83+
/**
84+
* Returns a [Deadline] that passes after given [timeout] from now.
85+
*
86+
* The timeout **must** be positive.
87+
*/
88+
fun after(timeout: Duration): Deadline {
89+
require(timeout > Duration.ZERO) { "Timeout must be positive: $timeout" }
90+
return Deadline(now() + timeout)
91+
}
92+
93+
}
94+
95+
}
96+
97+
/**
98+
* Runs a given suspending [block] of code inside a coroutine with a specified [deadline][Deadline]
99+
* and throws a [TimeoutCancellationException] when the deadline passes.
100+
*
101+
* If current deadline is less than the specified one, it will be used instead.
102+
*/
103+
suspend fun <R> withDeadline(deadline: Deadline, block: suspend CoroutineScope.() -> R): R {
104+
val currentDeadline = coroutineContext[Deadline]
105+
val newDeadline = currentDeadline
106+
?.let { minOf(it, deadline) }
107+
?: deadline
108+
109+
return withTimeout(newDeadline.timeLeft()) {
110+
withContext(newDeadline, block)
111+
}
112+
}
113+
114+
/**
115+
* Shortcut for `withDeadline(Deadline.after(duration))`.
116+
*
117+
* @see withDeadline
118+
* @see Deadline.after
119+
*/
120+
suspend fun <R> withDeadlineAfter(timeout: Duration, block: suspend CoroutineScope.() -> R): R {
121+
val deadline = Deadline.after(timeout)
122+
return withDeadline(deadline, block)
123+
}

src/main/kotlin/ru/kontur/kinfra/commons/time/MonotonicInstant.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,49 @@ import kotlin.time.*
1111
* To be replaced by [ClockMark] of [MonoClock] after its final release.
1212
*/
1313
class MonotonicInstant private constructor(
14-
private val nanos: Long
14+
private val nanoOffset: Long
1515
) : Comparable<MonotonicInstant> {
1616
// todo: deprecate after release of kotlin.time
1717

1818
operator fun plus(duration: Duration): MonotonicInstant {
19-
return MonotonicInstant(nanos + duration.toNanos())
19+
return MonotonicInstant(nanoOffset + duration.toNanos())
2020
}
2121

2222
operator fun minus(duration: Duration): MonotonicInstant {
23-
return MonotonicInstant(nanos - duration.toNanos())
23+
return MonotonicInstant(nanoOffset - duration.toNanos())
2424
}
2525

2626
operator fun minus(other: MonotonicInstant): Duration {
27-
return Duration.ofNanos(nanos - other.nanos)
27+
return Duration.ofNanos(nanoOffset - other.nanoOffset)
2828
}
2929

3030
override fun compareTo(other: MonotonicInstant): Int {
31-
return (nanos - other.nanos).compareTo(0)
31+
// implies that nanoOffset is monotonic
32+
return nanoOffset.compareTo(other.nanoOffset)
3233
}
3334

3435
override fun equals(other: Any?): Boolean {
35-
return other is MonotonicInstant && nanos == other.nanos
36+
return other is MonotonicInstant && nanoOffset == other.nanoOffset
3637
}
3738

3839
override fun hashCode(): Int {
39-
return nanos.hashCode()
40+
return nanoOffset.hashCode()
41+
}
42+
43+
override fun toString(): String {
44+
return "MonotonicInstant(${Duration.ofSeconds(0, nanoOffset)} since origin)"
4045
}
4146

4247
companion object {
4348

49+
private val ORIGIN_NANOS = rawNow()
50+
4451
fun now(): MonotonicInstant {
45-
return MonotonicInstant(System.nanoTime())
52+
return MonotonicInstant(rawNow() - ORIGIN_NANOS)
53+
}
54+
55+
private fun rawNow(): Long {
56+
return System.nanoTime()
4657
}
4758

4859
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package ru.kontur.kinfra.commons.time
2+
3+
import kotlinx.coroutines.runBlocking
4+
import org.assertj.core.api.Assertions.assertThat
5+
import org.junit.jupiter.api.Test
6+
import org.junit.jupiter.api.assertThrows
7+
import java.time.Duration
8+
9+
class DeadlineTest {
10+
11+
@Test
12+
fun plus_minus() {
13+
val duration = Duration.ofSeconds(1)
14+
val sample = Deadline.after(duration)
15+
val earlier = sample - duration
16+
val later = sample + duration
17+
18+
assertThat(earlier).isLessThan(sample)
19+
assertThat(later).isGreaterThan(sample)
20+
assertThat(earlier + duration.multipliedBy(2)).isEqualTo(later)
21+
}
22+
23+
@Test
24+
fun is_passed_nanosecond() {
25+
val deadline = Deadline.after(Duration.ofNanos(1))
26+
assertThat(deadline).matches { it.isPassed() }
27+
}
28+
29+
@Test
30+
fun is_passed_second() {
31+
val deadline = Deadline.after(Duration.ofSeconds(1))
32+
assertThat(deadline).matches { !it.isPassed() }
33+
}
34+
35+
@Test
36+
fun withDeadline_use_earlier() = runBlocking<Unit> {
37+
val earlier = Deadline.after(Duration.ofSeconds(1))
38+
val later = Deadline.after(Duration.ofSeconds(2))
39+
40+
withDeadline(earlier) {
41+
withDeadline(later) {
42+
assertThat(coroutineContext[Deadline]).isEqualTo(earlier)
43+
}
44+
}
45+
46+
withDeadline(later) {
47+
withDeadline(earlier) {
48+
assertThat(coroutineContext[Deadline]).isEqualTo(earlier)
49+
}
50+
}
51+
}
52+
53+
@Test
54+
fun time_left_instantly() {
55+
val timeout = Duration.ofSeconds(1)
56+
val deadline = Deadline.after(timeout)
57+
val timeLeft = deadline.timeLeft()
58+
val maxDifference = Duration.ofMillis(20)
59+
60+
assertThat(timeLeft).isLessThanOrEqualTo(timeout)
61+
assertThat(timeLeft).isGreaterThan(timeout - maxDifference)
62+
}
63+
64+
@Test
65+
fun time_left_after_delay() {
66+
val timeout = Duration.ofSeconds(1)
67+
val deadline = Deadline.after(timeout)
68+
69+
val delay = Duration.ofMillis(50)
70+
Thread.sleep(delay.toMillis())
71+
val timeLeft = deadline.timeLeft()
72+
73+
assertThat(timeLeft).isLessThanOrEqualTo(timeout - delay)
74+
assertThat(timeLeft).isGreaterThan(timeout - delay.multipliedBy(2))
75+
}
76+
77+
@Test
78+
fun zero_timeout_forbidden() {
79+
assertThrows<IllegalArgumentException> {
80+
Deadline.after(Duration.ZERO)
81+
}
82+
}
83+
84+
@Test
85+
fun negative_timeout_forbidden() {
86+
assertThrows<IllegalArgumentException> {
87+
Deadline.after(Duration.ofSeconds(1).negated())
88+
}
89+
}
90+
91+
}

0 commit comments

Comments
 (0)