From a49eca7991bf1cf9f8eced93878db270b6a31c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=9A=D1=83=D1=80=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2?= Date: Thu, 8 Oct 2026 16:46:04 +0300 Subject: [PATCH] solve: add task 1 (snowflake generator) --- src/part1/constants.py | 44 ++++----------- src/part1/snowflake.py | 122 +++++++++-------------------------------- test/test_snowflake.py | 41 ++++++++++++++ 3 files changed, 80 insertions(+), 127 deletions(-) create mode 100644 test/test_snowflake.py diff --git a/src/part1/constants.py b/src/part1/constants.py index 9abfbba2..095f20d0 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -1,38 +1,18 @@ -"""Bit layout and default epoch for the Twitter Snowflake ID generator. +def my_max_value(n: int) -> int: + return int(2**n - 1) -A Snowflake identifier is a 63-bit positive integer packed as follows, from -the most significant bit downwards: - 1 bit unused sign bit, always zero - 41 bits milliseconds elapsed since a custom epoch - 10 bits node identifier - 12 bits per-millisecond sequence counter +EPOCH_MS_DEFAULT = 1288834974657 +NODE_ID_DEFAULT = 1 -The shift and mask constants should be derived from the bit widths so that -changing a width keeps everything else consistent. -""" -from typing import Final +TIMESTAMP_BITS = 41 # число миллисекунд прощедщих с момента пользовательской эпохи +NODE_ID_BITS = 10 # номер узла +SEQUENCE_ID_BITS = 12 # порядковый номер +TIMESTAMP_SHIFT = NODE_ID_BITS + SEQUENCE_ID_BITS -# TODO: замените заглушки (0) на корректные значения, см. TASK.md. -# Все константы, зависящие от ёмкостей *_BITS, должны вычисляться из них. - -# Twitter's original Snowflake epoch: 2010-11-04 01:42:54.657 UTC. -EPOCH_MS_DEFAULT: Final[int] = 0 - -# Node identifier used when the caller does not supply one. -NODE_ID_DEFAULT: Final[int] = 0 - -# Width of each field, in bits. -TIMESTAMP_BITS: Final[int] = 0 -NODE_ID_BITS: Final[int] = 0 -SEQUENCE_ID_BITS: Final[int] = 0 - -# Largest value each field can hold. -TIMESTAMP_MS_MAX: Final[int] = 0 -NODE_ID_MAX: Final[int] = 0 -SEQUENCE_ID_MAX: Final[int] = 0 - -# Здесь можно добавить собственные вспомогательные константы -# (например, сдвиги полей при сборке идентификатора). +# так как номер и время(все три строчки) беззнаковые +TIMESTAMP_MS_MAX = my_max_value(TIMESTAMP_BITS) +NODE_ID_MAX = my_max_value(NODE_ID_BITS) +SEQUENCE_ID_MAX = my_max_value(SEQUENCE_ID_BITS) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index d3abf050..95ca761c 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,114 +1,46 @@ -"""Twitter Snowflake identifier generator. +import time -Snowflake produces roughly time-ordered 64-bit integer identifiers without -coordination between nodes. Each identifier packs the milliseconds elapsed -since a custom epoch, a node identifier and a per-millisecond sequence counter -into a single 63-bit positive integer (see ``constants`` for the layout). - -The public entry point is :func:`generate_snowflake_id`. It is stateless: the -caller passes the sequence counter on every call and is responsible for -advancing it within a millisecond and resetting it when the clock ticks over. - -Each packed field can be read back on its own with :func:`decode_timestamp_ms`, -:func:`decode_node_id` and :func:`decode_sequence_id`. -""" - -import time # noqa: F401 - -from .constants import ( # noqa: F401 - EPOCH_MS_DEFAULT, - NODE_ID_DEFAULT, - NODE_ID_MAX, - SEQUENCE_ID_MAX, - TIMESTAMP_MS_MAX, -) +from .constants import * def read_current_millis(epoch_ms: int) -> int: - """Read the number of milliseconds elapsed since the custom epoch. - - Args: - epoch_ms: The custom epoch expressed as Unix milliseconds. - - Returns: - The count of whole milliseconds between ``epoch_ms`` and now. May be - negative if ``epoch_ms`` lies in the future. - """ - # TODO: реализуйте функцию - return 0 + now_ms = time.time_ns() // 1_000_000 # текущий моент + return now_ms - epoch_ms def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - """Read the timestamp field out of a Snowflake identifier. - - Args: - snowflake_id: An identifier produced against the same epoch. - epoch_ms: The epoch the identifier was generated against. Defaults to - the original Twitter epoch (2010-11-04 01:42:54.657 UTC). - - Returns: - The absolute Unix time in milliseconds at which the identifier was - generated. - """ - # TODO: реализуйте функцию - return 0 + return (snowflake_id >> TIMESTAMP_SHIFT) + epoch_ms def decode_node_id(snowflake_id: int) -> int: - """Read the node identifier field out of a Snowflake identifier. - - Args: - snowflake_id: An identifier produced by :func:`generate_snowflake_id`. - - Returns: - The node identifier packed into ``snowflake_id``, in the range - ``[0, NODE_ID_MAX]``. - """ - # TODO: реализуйте функцию - return 0 + return (snowflake_id >> SEQUENCE_ID_BITS) & NODE_ID_MAX def decode_sequence_id(snowflake_id: int) -> int: - """Read the sequence counter field out of a Snowflake identifier. + return snowflake_id & SEQUENCE_ID_MAX - Args: - snowflake_id: An identifier produced by :func:`generate_snowflake_id`. - - Returns: - The per-millisecond sequence counter packed into ``snowflake_id``, in - the range ``[0, SEQUENCE_ID_MAX]``. - """ - # TODO: реализуйте функцию - return 0 + # главная функция def generate_snowflake_id( - sequence_id: int, - node_id: int = NODE_ID_DEFAULT, - epoch_ms: int = EPOCH_MS_DEFAULT, + sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT ) -> int | None: - """Build and return a Snowflake identifier for the current millisecond. - - The function is stateless: the caller passes the per-millisecond sequence - counter explicitly. It is the caller's responsibility to increment - ``sequence_id`` for identifiers minted within the same millisecond and to - reset it once the clock advances. - - Args: - sequence_id: The per-millisecond sequence counter, in the range - ``[0, SEQUENCE_ID_MAX]``. - node_id: The identifier of this node, in the range ``[0, NODE_ID_MAX]``. - Optional; defaults to ``NODE_ID_DEFAULT``. - epoch_ms: The start of the epoch as Unix milliseconds. Optional; - defaults to the original Twitter epoch (2010-11-04 01:42:54.657 - UTC). - Returns: - The Snowflake identifier as a positive 63-bit integer, or ``None`` if - ``node_id`` is outside ``[0, NODE_ID_MAX]``, ``sequence_id`` is outside - ``[0, SEQUENCE_ID_MAX]``, or the elapsed time no longer fits in the - timestamp field (roughly 69 years after ``epoch_ms``). In each of those - cases an explanatory message is printed to stdout first. - """ - # TODO: реализуйте функцию - return 0 + local_const_time = read_current_millis(epoch_ms) + + if node_id < 0 or node_id > NODE_ID_MAX: + print(f"node_id must be in [0, {NODE_ID_MAX}]") + return None + if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: + print(f"sequence_id must be in [0, {SEQUENCE_ID_MAX}]") + return None + if local_const_time > TIMESTAMP_MS_MAX: + print("overflows time") + return None + + snowflake_id = ( + (local_const_time << TIMESTAMP_SHIFT) + | (node_id << SEQUENCE_ID_BITS) + | sequence_id + ) + return snowflake_id diff --git a/test/test_snowflake.py b/test/test_snowflake.py new file mode 100644 index 00000000..16deb8f7 --- /dev/null +++ b/test/test_snowflake.py @@ -0,0 +1,41 @@ +from part1.constants import EPOCH_MS_DEFAULT, NODE_ID_MAX, SEQUENCE_ID_MAX +from part1.snowflake import ( + decode_node_id, + decode_sequence_id, + decode_timestamp_ms, + generate_snowflake_id, +) + + +def test_roundtrip_node_and_sequence(): + for node in [0, 1, 42, 500, NODE_ID_MAX]: + for seq in [0, 1, 100, 4000, SEQUENCE_ID_MAX]: + sid = generate_snowflake_id(seq, node) + assert sid is not None + assert decode_node_id(sid) == node + assert decode_sequence_id(sid) == seq + + +def test_timestamp_close_to_now(): + import time + before = time.time_ns() // 1_000_000 + sid = generate_snowflake_id(0, 1) + after = time.time_ns() // 1_000_000 + + ts = decode_timestamp_ms(sid) + assert before <= ts <= after + + +def test_positive_64bit(): + sid = generate_snowflake_id(SEQUENCE_ID_MAX, NODE_ID_MAX) + assert 0 < sid < 2**63 + + +def test_invalid_node(): + assert generate_snowflake_id(0, NODE_ID_MAX + 1) is None + assert generate_snowflake_id(0, -1) is None + + +def test_invalid_sequence(): + assert generate_snowflake_id(SEQUENCE_ID_MAX + 1, 1) is None + assert generate_snowflake_id(-1, 1) is None