Change posts to use random-ish ids
parent
abb1dc17fd
commit
a9ce2c0511
|
@ -2,5 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class PostConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
default_auto_field = "printpub.post.id.IDField"
|
||||
name = "printpub.post"
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import math
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import django.db.models
|
||||
|
||||
|
||||
def generate():
|
||||
"""
|
||||
Generate an identifier that is "unique" (has some amount of random bits, but is not totally random)
|
||||
|
||||
Inspired heavily by https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c
|
||||
but without the sharding
|
||||
"""
|
||||
EPOCH = 1600000000000
|
||||
# Using 42 bits for the time means that we won't see a rollover here for over 200 years
|
||||
# (2**43 minus the time this was written (07/10/23), minus our epoch, gives 276 years in ms).
|
||||
# We're not trying to recover the bits here anyway, though, so perhaps in 300 years this won't matter anyway.
|
||||
TIME_SIZE = 42
|
||||
# The remaining 22 bits can be used for randomness (this means we will have 2**22 ids possible per ms, which is plenty.)
|
||||
random_part = secrets.randbits(64 - TIME_SIZE)
|
||||
now_millis = math.floor(time.time() * 1000)
|
||||
|
||||
return ((now_millis - EPOCH) << (64 - TIME_SIZE)) | random_part
|
||||
|
||||
|
||||
class IDField(django.db.models.BigAutoField):
|
||||
"""
|
||||
An identifier for a post which is partially random, and partially based on the timestamp.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
upd_kwargs = {**kwargs, "default": generate}
|
||||
super().__init__(*args, **upd_kwargs)
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.2.3 on 2023-07-10 23:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import printpub.post.id
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("post", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="post",
|
||||
name="id",
|
||||
field=printpub.post.id.IDField(
|
||||
auto_created=True,
|
||||
default=printpub.post.id.generate,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,16 @@
|
|||
import unittest.mock
|
||||
|
||||
import printpub.post.id
|
||||
|
||||
|
||||
def test_id_generation_has_unique_and_nonunique_part():
|
||||
now_seconds = 1689031817.358745
|
||||
with unittest.mock.patch("time.time", return_value=now_seconds):
|
||||
id1 = printpub.post.id.generate()
|
||||
id2 = printpub.post.id.generate()
|
||||
|
||||
time_mask = int("1" * 42 + "0" * (64 - 42), 2)
|
||||
# The first 42 bits should be the time
|
||||
assert id1 & time_mask == id2 & time_mask
|
||||
# The last bits should be random
|
||||
assert id1 & (~time_mask) != id2 & (~time_mask)
|
Loading…
Reference in New Issue