From a9ce2c0511150af56302b42fb379a1fe144aa483 Mon Sep 17 00:00:00 2001 From: Nick Krichevsky Date: Mon, 10 Jul 2023 19:52:39 -0400 Subject: [PATCH] Change posts to use random-ish ids --- printpub/post/apps.py | 2 +- printpub/post/id.py | 34 +++++++++++++++++++ .../post/migrations/0002_alter_post_id.py | 25 ++++++++++++++ tests/post/id_test.py | 16 +++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 printpub/post/id.py create mode 100644 printpub/post/migrations/0002_alter_post_id.py create mode 100644 tests/post/id_test.py diff --git a/printpub/post/apps.py b/printpub/post/apps.py index 939c84d..2d86189 100644 --- a/printpub/post/apps.py +++ b/printpub/post/apps.py @@ -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" diff --git a/printpub/post/id.py b/printpub/post/id.py new file mode 100644 index 0000000..f896f7e --- /dev/null +++ b/printpub/post/id.py @@ -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) diff --git a/printpub/post/migrations/0002_alter_post_id.py b/printpub/post/migrations/0002_alter_post_id.py new file mode 100644 index 0000000..8f61c9b --- /dev/null +++ b/printpub/post/migrations/0002_alter_post_id.py @@ -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", + ), + ), + ] diff --git a/tests/post/id_test.py b/tests/post/id_test.py new file mode 100644 index 0000000..cfb056c --- /dev/null +++ b/tests/post/id_test.py @@ -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)