Skip to content

Commit

Permalink
Migrate to kotlinx-datetime (#297)
Browse files Browse the repository at this point in the history
* Migrate API code to kotlinx-datetime

* Update tests

* Remove dead code

* Replace iso serializing with kx.dt
  • Loading branch information
DRSchlaubi authored May 20, 2021
1 parent 5be0951 commit 81694e7
Show file tree
Hide file tree
Showing 37 changed files with 126 additions and 135 deletions.
2 changes: 2 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ object Versions {
const val ktor = "1.5.3"
const val kotlinxCoroutines = "1.5.0-RC"
const val kotlinLogging = "2.0.4"
const val dateTime = "0.2.0"
const val atomicFu = "0.16.1"
const val binaryCompatibilityValidator = "0.4.0"

Expand All @@ -28,6 +29,7 @@ object Dependencies {
"org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}"
const val `kotlinx-coroutines` = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinxCoroutines}"
const val `kotlinx-atomicfu` = "org.jetbrains.kotlinx:atomicfu-jvm:${Versions.atomicFu}"
const val `kotlinx-datetime` = "org.jetbrains.kotlinx:kotlinx-datetime:${Versions.dateTime}"

const val `kotlin-logging` = "io.github.microutils:kotlin-logging:${Versions.kotlinLogging}"

Expand Down
4 changes: 4 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ configurations {
}
}

dependencies {
api(Dependencies.`kotlinx-datetime`)
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = Jvm.target
Expand Down
14 changes: 6 additions & 8 deletions common/src/main/kotlin/entity/Snowflake.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package dev.kord.common.entity

import kotlinx.datetime.Clock
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
import kotlinx.datetime.Instant
import kotlin.time.Duration
import kotlin.time.TimeMark
import kotlin.time.toKotlinDuration

/**
* A unique identifier for entities [used by discord](https://discord.com/developers/docs/reference#snowflakes).
Expand All @@ -28,11 +28,11 @@ class Snowflake(val value: Long) : Comparable<Snowflake> {
/**
* Creates a Snowflake from a given [instant].
*/
constructor(instant: Instant) : this((instant.toEpochMilli() shl 22) - discordEpochLong)
constructor(instant: Instant) : this((instant.toEpochMilliseconds() shl 22) - discordEpochLong)

val asString get() = value.toString()

val timeStamp: Instant get() = Instant.ofEpochMilli(discordEpochLong + (value shr 22))
val timeStamp: Instant get() = Instant.fromEpochMilliseconds(discordEpochLong + (value shr 22))

val timeMark: TimeMark get() = SnowflakeMark(value shr 22)

Expand All @@ -48,7 +48,7 @@ class Snowflake(val value: Long) : Comparable<Snowflake> {

companion object {
private const val discordEpochLong = 1420070400000L
val discordEpochStart: Instant = Instant.ofEpochMilli(discordEpochLong)
val discordEpochStart: Instant = Instant.fromEpochMilliseconds(discordEpochLong)

/**
* The maximum value a Snowflake can hold.
Expand Down Expand Up @@ -78,7 +78,5 @@ class Snowflake(val value: Long) : Comparable<Snowflake> {

private class SnowflakeMark(val epochMilliseconds: Long) : TimeMark() {

override fun elapsedNow(): Duration =
java.time.Duration.between(Instant.ofEpochMilli(epochMilliseconds), Instant.now()).toKotlinDuration()

override fun elapsedNow(): Duration = Instant.fromEpochMilliseconds(epochMilliseconds) - Clock.System.now()
}
16 changes: 7 additions & 9 deletions common/src/main/kotlin/ratelimit/BucketRateLimiter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package dev.kord.common.ratelimit

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Clock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration
import kotlin.time.milliseconds
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration


/**
Expand All @@ -19,30 +17,30 @@ import java.time.Duration as JavaDuration
class BucketRateLimiter(
private val capacity: Int,
private val refillInterval: Duration,
private val clock: Clock = Clock.systemUTC()
private val clock: Clock = Clock.System
) : RateLimiter {

private val mutex = Mutex()

private var count = 0
private var nextInterval = 0L
private var nextInterval = Instant.fromEpochMilliseconds(0)

init {
require(capacity > 0) { "capacity must be a positive number" }
require(refillInterval.isPositive()) { "refill interval must be positive" }
}

private val isNextInterval get() = nextInterval <= clock.millis()
private val isNextInterval get() = nextInterval <= clock.now()

private val isAtCapacity get() = count == capacity

private fun resetState() {
count = 0
nextInterval = clock.millis() + refillInterval.inWholeMilliseconds
nextInterval = clock.now() + refillInterval
}

private suspend fun delayUntilNextInterval() {
val delay = nextInterval - clock.millis()
val delay = nextInterval - clock.now()
kotlinx.coroutines.delay(delay)
}

Expand Down
8 changes: 8 additions & 0 deletions common/src/test/kotlin/FixedClock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

fun Clock.Companion.fixed(instant: Instant): Clock = FixedClock(instant)

private class FixedClock(private val instant: Instant): Clock {
override fun now(): Instant = instant
}
10 changes: 5 additions & 5 deletions common/src/test/kotlin/ratelimit/BucketRateLimiterTest.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package ratelimit

import dev.kord.common.ratelimit.BucketRateLimiter
import fixed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import java.time.Clock
import java.time.Instant
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.time.ZoneOffset
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.asserter
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.milliseconds

@ExperimentalTime
@ExperimentalCoroutinesApi
class BucketRateLimiterTest {

val interval = Duration.milliseconds(1_000_000)
val instant = Instant.now()
val clock = Clock.fixed(instant, ZoneOffset.UTC)
val instant = Clock.System.now()
val clock = Clock.fixed(instant)
lateinit var rateLimiter: BucketRateLimiter

@BeforeTest
Expand Down
8 changes: 3 additions & 5 deletions core/src/main/kotlin/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import dev.kord.rest.json.JsonErrorCode
import dev.kord.rest.request.RestRequestException
import dev.kord.rest.route.Position
import kotlinx.coroutines.flow.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.datetime.Instant
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
Expand All @@ -38,9 +37,8 @@ internal fun Long?.toSnowflakeOrNull(): Snowflake? = when {
else -> Snowflake(this)
}

internal fun String.toInstant() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this, Instant::from)
internal fun Int.toInstant() = Instant.ofEpochMilli(toLong())
internal fun Long.toInstant() = Instant.ofEpochMilli(this)
internal fun Int.toInstant() = Instant.fromEpochMilliseconds(toLong())
internal fun Long.toInstant() = Instant.fromEpochMilliseconds(this)

@OptIn(ExperimentalContracts::class)
internal inline fun <T> catchNotFound(block: () -> T): T? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import dev.kord.rest.request.RestRequestException
import dev.kord.rest.service.RestClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.time.Duration
import java.time.Instant
import kotlinx.datetime.Clock
import kotlin.time.Duration
import kotlinx.datetime.Instant
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
Expand Down Expand Up @@ -70,11 +71,11 @@ interface GuildMessageChannelBehavior : GuildChannelBehavior, MessageChannelBeha
* @throws [RestRequestException] if something went wrong during the request.
*/
suspend fun bulkDelete(messages: Iterable<Snowflake>) {
val daysLimit = Instant.now() - Duration.ofDays(14)
val daysLimit = Clock.System.now() - Duration.days(14)
//split up in bulk delete and manual delete
// if message.timeMark + 14 days > now, then the message isn't 14 days old yet, and we can add it to the bulk delete
// if message.timeMark + 14 days < now, then the message is more than 14 days old, and we'll have to manually delete them
val (younger, older) = messages.partition { it.timeStamp.isAfter(daysLimit) }
val (younger, older) = messages.partition { it.timeStamp > daysLimit }

younger.chunked(100).forEach {
if (it.size < 2) kord.rest.channel.deleteMessage(id, it.first())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import java.time.Instant
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
Expand Down Expand Up @@ -210,7 +211,7 @@ interface MessageChannelBehavior : ChannelBehavior, Strategizable {
* @throws [RestRequestException] if something went wrong during the request.
*/
suspend fun typeUntil(instant: Instant) {
while (instant.isBefore(Instant.now())) {
while (instant < Clock.System.now()) {
type()
delay(Duration.seconds(8).inWholeMilliseconds) //bracing ourselves for some network delays
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/entity/Activity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import dev.kord.common.entity.*
import dev.kord.common.entity.optional.value
import dev.kord.core.cache.data.ActivityData
import dev.kord.core.toInstant
import java.time.Instant
import kotlinx.datetime.Instant

class Activity(val data: ActivityData) {

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/entity/Embed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import dev.kord.common.entity.optional.value
import dev.kord.core.Kord
import dev.kord.core.KordObject
import dev.kord.core.cache.data.*
import dev.kord.core.toInstant
import dev.kord.rest.builder.message.EmbedBuilder
import java.time.Instant
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant

internal const val embedDeprecationMessage = """
Embed types should be considered deprecated and might be removed in a future API version.
Expand Down
11 changes: 3 additions & 8 deletions core/src/main/kotlin/entity/Guild.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import dev.kord.core.supplier.getChannelOfOrNull
import dev.kord.rest.Image
import dev.kord.rest.service.RestClient
import kotlinx.coroutines.flow.first
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
import java.util.*

/**
Expand Down Expand Up @@ -182,12 +182,7 @@ class Guild(
* The time at which this guild was joined, if present.
*/
val joinedTime: Instant?
get() = data.joinedAt.value?.let {
DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(
it,
Instant::from
)
}
get() = data.joinedAt.value?.toInstant()

/**
* The id of the owner.
Expand Down
9 changes: 4 additions & 5 deletions core/src/main/kotlin/entity/Integration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import dev.kord.core.cache.data.IntegrationData
import dev.kord.core.exception.EntityNotFoundException
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.core.toInstant
import dev.kord.rest.builder.integration.IntegrationModifyBuilder
import dev.kord.rest.request.RestRequestException
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.time.Duration
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
Expand Down Expand Up @@ -99,7 +98,7 @@ class Integration(
* The grace period in days before expiring subscribers.
*/
val expireGracePeriod: Duration
get() = Duration.of(data.expireGracePeriod.toLong(), ChronoUnit.DAYS)
get() = Duration.days(data.expireGracePeriod)

/**
* The id of the [user][User] for this integration.
Expand Down
7 changes: 3 additions & 4 deletions core/src/main/kotlin/entity/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import dev.kord.core.cache.data.MemberData
import dev.kord.core.cache.data.UserData
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.core.toInstant
import kotlinx.coroutines.flow.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.datetime.toInstant
import kotlinx.datetime.Instant
import java.util.*

/**
Expand All @@ -39,7 +38,7 @@ class Member(
/**
* When the user joined this [guild].
*/
val joinedAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(memberData.joinedAt, Instant::from)
val joinedAt: Instant get() = memberData.joinedAt.toInstant()

/**
* The guild-specific nickname of the user, if present.
Expand Down
10 changes: 4 additions & 6 deletions core/src/main/kotlin/entity/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.core.supplier.getChannelOf
import dev.kord.core.supplier.getChannelOfOrNull
import kotlinx.coroutines.flow.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
import java.util.*

/**
Expand Down Expand Up @@ -70,9 +70,7 @@ class Message(
* Returns null if the message was never edited.
*/
val editedTimestamp: Instant?
get() = data.editedTimestamp?.let {
DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(it, Instant::from)
}
get() = data.editedTimestamp?.toInstant()

/**
* The embedded content of this message.
Expand Down Expand Up @@ -205,7 +203,7 @@ class Message(
/**
* The instant when this message was created.
*/
val timestamp: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.timestamp, Instant::from)
val timestamp: Instant get() = data.timestamp.toInstant()

/**
* Whether this message was send using `\tts`.
Expand Down
8 changes: 4 additions & 4 deletions core/src/main/kotlin/entity/Template.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import dev.kord.core.Kord
import dev.kord.core.KordObject
import dev.kord.core.behavior.TemplateBehavior
import dev.kord.core.cache.data.TemplateData
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant

class Template(val data: TemplateData, override val kord: Kord) : KordObject, TemplateBehavior {
override val code: String get() = data.code
Expand All @@ -21,9 +21,9 @@ class Template(val data: TemplateData, override val kord: Kord) : KordObject, Te

val creator: User get() = User(data.creator, kord)

val createdAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.createdAt, Instant::from)
val createdAt: Instant get() = data.createdAt.toInstant()

val updatedAt: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.updatedAt, Instant::from)
val updatedAt: Instant get() = data.updatedAt.toInstant()

override val guildId: Snowflake get() = data.sourceGuildId

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/entity/channel/MessageChannel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import dev.kord.core.behavior.MessageBehavior
import dev.kord.core.behavior.channel.MessageChannelBehavior
import dev.kord.core.entity.Message
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.core.toInstant
import java.time.Instant
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant

/**
* An instance of a Discord channel that can use messages.
Expand Down
Loading

0 comments on commit 81694e7

Please sign in to comment.