-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes and improvements for Snowflake #370
Conversation
…fore shifting bits to the left).
…re this, Snowflake.min.timeMark.hasPassedNow() returned false and Snowflake.max.timeMark.hasPassedNow() returned true).
…Epoch to SnowflakeMark's constructor by using same math as Snowflake.timeStamp. Example where this comes up: Snowflake(Clock.System.now()).timeMark.elapsedNow() should return a very short duration, but did not before.
…tionality behave correctly for Long values smaller than 0 (having 1 as the first bit). Maybe ULong should be used for Snowflake.value? (Would bring more accurate Snowflake.max with ULong.MAX_VALUE, avoid confusion with negative Snowflake values etc. but I'm not sure about breaking changes and serialization.)
Thanks for bringing to light this series of horrible mistakes and taking your time to fix them.
Yes. Since 1.5.0 unsigned integers are stable and inline classes are serializable, so we do have that option now. Seeing as Snowflakes can never be negative, this seems like a good idea overall. Also, if it's not too much to ask, could you write up a few tests for your fixes? It'd be a good idea to get these fixes solidified as regression tests. |
Sure, I will try to write some tests later today. |
… constant, more documentation
I did add some tests, but to make I've also looked a bit into serialization of inline classes and this seems to be a way to serialize a Snowflake with an @OptIn(ExperimentalSerializationApi::class)
internal class Serializer : KSerializer<Snowflake> {
override val descriptor: SerialDescriptor
get() = @OptIn(ExperimentalUnsignedTypes::class) ULong.serializer().descriptor
override fun deserialize(decoder: Decoder): Snowflake = Snowflake(decoder.decodeInline(descriptor).decodeLong().toULong())
override fun serialize(encoder: Encoder, value: Snowflake) {
encoder.encodeInline(descriptor).encodeLong(value.value.toLong())
}
} As you can see, it still seems to be experimental, so I'm not sure if it can be used by Kord yet. |
Ok, I've got a working version with |
Yes, if they serve the PR |
Alright, I pushed the changes, would be nice if someone could have a look at them. |
class Snowflake(val value: Long) : Comparable<Snowflake> { | ||
class Snowflake(val value: ULong) : Comparable<Snowflake> { | ||
|
||
/** | ||
* Creates a Snowflake from a given String [value], parsing it a [Long] value. | ||
* Creates a Snowflake from a given String [value], parsing it as a [ULong] value. | ||
*/ | ||
constructor(value: String) : this(value.toLong()) | ||
constructor(value: String) : this(value.toULong()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest retaining a Long
constructor for ease of use and migration, I imagine plenty of people have a Snowflake(long)
somewhere defined as a constant and we're a far ways off from reaching the most significant bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I will add this, but two decisions are needed:
- Should it use invoke for companion object or a function as a pseudo constructor (normal constructor will lead to conflicting JVM overloads)?
- Should it throw on negative values or just use a
ULong
with the same binary representation (meaning -1 will give aSnowflake
with maximum timestamp)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be ok to throw an exception if I add @Deprecated
to the pseudo constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it use invoke for companion object or a function as a pseudo constructor (normal constructor will lead to conflicting JVM overloads)?
a top-level factory function. We're trying to move away from companion invokes ever since the naming rules for factory function changed.
Should it throw on negative values or just use a ULong with the same binary representation (meaning -1 will give a Snowflake with maximum timestamp)?
The scenario shouldn't happen all that, so I'm not really concerned with breaking changes. However, negative values were valid (although undocumented) before this change. It might be best to just handle them as unsigned values as well.
I think it would be ok to throw an exception if I add @deprecated to the pseudo constructor.
I wouldn't want it to be marked Deprecated, the whole point of adding the faux constructor is convenience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
This should be all changes for now. I updated the PR description to include all notable ones. Please tell me, if you think I missed something. |
Fixes for
Snowflake
Instant
took the epoch milliseconds from the instant, shifted it to the left and then substracted the Discord Epoch but it should first subtract and then shift (see https://discord.com/developers/docs/reference#snowflake-ids-in-pagination-generating-a-snowflake-id-from-a-timestamp-example) -- fixed in 76a35b6SnowflakeMark.elapsedNow()
returned aDuration
with the wrong sign (Snowflake.min.timeMark.hasPassedNow()
returnedfalse
andSnowflake.max.timeMark.hasPassedNow()
returnedtrue
) -- fixed in b0bafb8Snowflake.timeMark
passed unadjusted milliseconds since Discord Epoch toSnowflakeMark
's constructor which e.g. causedSnowflake(Clock.System.now()).timeMark.elapsedNow()
to return a very longDuration
where it should return a very short one -- fixed in 86d1412 (and deduplicated logic fromtimeStamp
andtimeMark
in c47caa0)Improvements for
Snowflake
ad0d746: using- replaced by c889ff9ushr
instead ofshr
to get correct behavior forSnowflake
s with a negativevalue
- Would it be a good idea to just useULong
forvalue
? (It would bring a more accurateSnowflake.max
withULong.MAX_VALUE
, avoid confusion with negativeSnowflake
values etc. But I'm not sure if this would break things elsewhere and if/how it would work with serialization.)compareTo()
andequals()
behavior forSnowflake
.Snowflake
constructed withInstant
too far in the past/future has min/max timestamp instead of undefined overflow behavior,Snowflake.endOfTime
constant and more documentation.value
is nowULong
(wasLong
before) - this included modifying the serializer forSnowflake
s.Long
constructor (with additional note on negative values).