Change posts to use random-ish ids

master
Nick Krichevsky 2023-07-10 19:52:39 -04:00
parent abb1dc17fd
commit a9ce2c0511
4 changed files with 76 additions and 1 deletions

View File

@ -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"

34
printpub/post/id.py Normal file
View File

@ -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)

View File

@ -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",
),
),
]

16
tests/post/id_test.py Normal file
View File

@ -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)