Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace App\Framework\Ksuid;
use BcMath\Number;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* KSUID Value Object
*
* K-Sortable Unique Identifier
* - 160-bit (27 characters Base62)
* - 32-bit timestamp + 128-bit random payload
* - Lexicographically sortable by creation time
* - URL-safe, case-sensitive
*/
final readonly class Ksuid
{
public const int ENCODED_LENGTH = 27;
public const int TIMESTAMP_BYTES = 4;
public const int PAYLOAD_BYTES = 16;
public const int TOTAL_BYTES = self::TIMESTAMP_BYTES + self::PAYLOAD_BYTES; // 20
// KSUID epoch: 2014-05-13T16:53:20Z (Unix timestamp 1400000000)
public const int EPOCH = 1400000000;
// Base62 alphabet: 0-9, A-Z, a-z
public const string ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
private string $value;
private int $timestamp;
private string $payload;
public function __construct(string $value)
{
if (empty($value)) {
throw new InvalidArgumentException('KSUID cannot be empty');
}
if (strlen($value) !== self::ENCODED_LENGTH) {
throw new InvalidArgumentException('KSUID must be exactly ' . self::ENCODED_LENGTH . ' characters long');
}
if (! $this->isValidBase62($value)) {
throw new InvalidArgumentException('KSUID contains invalid characters');
}
$this->value = $value;
$this->parseComponents();
}
/**
* Create from raw bytes
*/
public static function fromBytes(string $bytes): self
{
if (strlen($bytes) !== self::TOTAL_BYTES) {
throw new InvalidArgumentException('KSUID bytes must be exactly ' . self::TOTAL_BYTES . ' bytes');
}
return new self(self::encodeBase62($bytes));
}
/**
* Create from string
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* Create from timestamp and payload
*/
public static function fromTimestampAndPayload(int $timestamp, string $payload): self
{
if (strlen($payload) !== self::PAYLOAD_BYTES) {
throw new InvalidArgumentException('Payload must be exactly ' . self::PAYLOAD_BYTES . ' bytes');
}
// Convert to KSUID timestamp (subtract epoch)
$ksuidTimestamp = $timestamp - self::EPOCH;
if ($ksuidTimestamp < 0) {
throw new InvalidArgumentException('Timestamp cannot be before KSUID epoch');
}
if ($ksuidTimestamp > 0xFFFFFFFF) {
throw new InvalidArgumentException('Timestamp overflow');
}
// Pack timestamp as big-endian 32-bit unsigned integer
$timestampBytes = pack('N', $ksuidTimestamp);
$bytes = $timestampBytes . $payload;
return self::fromBytes($bytes);
}
/**
* Get the string representation
*/
public function toString(): string
{
return $this->value;
}
/**
* Get the string value
*/
public function getValue(): string
{
return $this->value;
}
/**
* Magic method for string conversion
*/
public function __toString(): string
{
return $this->value;
}
/**
* Get the timestamp component (Unix timestamp)
*/
public function getTimestamp(): int
{
return $this->timestamp;
}
/**
* Get the timestamp as DateTime
*/
public function getDateTime(): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat('U', (string)$this->timestamp);
}
/**
* Get the raw payload bytes
*/
public function getPayload(): string
{
return $this->payload;
}
/**
* Get raw bytes representation
*/
public function getBytes(): string
{
return self::decodeBase62($this->value);
}
/**
* Check equality with another KSUID
*/
public function equals(self $other): bool
{
return $this->value === $other->value;
}
/**
* Compare with another KSUID for sorting
*/
public function compare(self $other): int
{
return strcmp($this->value, $other->value);
}
/**
* Check if this KSUID is older than another
*/
public function isOlderThan(self $other): bool
{
return $this->timestamp < $other->timestamp;
}
/**
* Check if this KSUID is newer than another
*/
public function isNewerThan(self $other): bool
{
return $this->timestamp > $other->timestamp;
}
/**
* Get age in seconds
*/
public function getAgeInSeconds(): int
{
return time() - $this->timestamp;
}
/**
* Validate Base62 characters
*/
private function isValidBase62(string $value): bool
{
$pattern = '/^[' . preg_quote(self::ALPHABET, '/') . ']+$/';
return preg_match($pattern, $value) === 1;
}
/**
* Parse timestamp and payload components from the value
*/
private function parseComponents(): void
{
$bytes = self::decodeBase62($this->value);
// Extract timestamp (first 4 bytes)
$timestampBytes = substr($bytes, 0, self::TIMESTAMP_BYTES);
$ksuidTimestamp = unpack('N', $timestampBytes)[1];
$this->timestamp = $ksuidTimestamp + self::EPOCH;
// Extract payload (remaining 16 bytes)
$this->payload = substr($bytes, self::TIMESTAMP_BYTES);
}
/**
* Encode bytes to Base62
*/
private static function encodeBase62(string $bytes): string
{
// Convert bytes to big integer using BcMath\Number
$num = new Number(0);
$base = new Number(1);
$base256 = new Number(256);
// Process each byte from right to left
for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
$byte = new Number(ord($bytes[$i]));
$num = $num->add($byte->mul($base));
$base = $base->mul($base256);
}
// Convert to Base62
$alphabet = self::ALPHABET;
$encoded = '';
$base62 = new Number(62);
$zero = new Number(0);
if ($num->compare($zero) === 0) {
return str_repeat($alphabet[0], self::ENCODED_LENGTH);
}
while ($num->compare($zero) > 0) {
[$quotient, $remainder] = $num->divmod($base62);
$encoded = $alphabet[(int)$remainder->__toString()] . $encoded;
$num = $quotient;
}
// Pad with leading zeros if necessary
return str_pad($encoded, self::ENCODED_LENGTH, $alphabet[0], STR_PAD_LEFT);
}
/**
* Decode Base62 to bytes
*/
private static function decodeBase62(string $encoded): string
{
$alphabet = self::ALPHABET;
$base62 = new Number(62);
$num = new Number(0);
$multiplier = new Number(1);
// Convert from Base62 to big integer
for ($i = strlen($encoded) - 1; $i >= 0; $i--) {
$char = $encoded[$i];
$value = strpos($alphabet, $char);
if ($value === false) {
throw new InvalidArgumentException('Invalid Base62 character: ' . $char);
}
$num = $num->add(new Number($value)->mul($multiplier));
$multiplier = $multiplier->mul($base62);
}
// Convert big integer to bytes
$bytes = '';
$base256 = new Number(256);
$zero = new Number(0);
while ($num->compare($zero) > 0) {
[$quotient, $remainder] = $num->divmod($base256);
$bytes = chr((int)$remainder->__toString()) . $bytes;
$num = $quotient;
}
// Pad with leading zero bytes if necessary
return str_pad($bytes, self::TOTAL_BYTES, "\0", STR_PAD_LEFT);
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Framework\Ksuid;
use App\Framework\Random\RandomGenerator;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* KSUID Generator Service
*
* Generates K-Sortable Unique Identifiers with timestamp ordering.
*/
final readonly class KsuidGenerator
{
public function __construct(
private RandomGenerator $randomGenerator
) {
}
/**
* Generate a new KSUID with current timestamp
*/
public function generate(): Ksuid
{
return $this->generateAt(time());
}
/**
* Generate a KSUID at a specific Unix timestamp
*/
public function generateAt(int $timestamp): Ksuid
{
if ($timestamp < Ksuid::EPOCH) {
throw new InvalidArgumentException('Timestamp cannot be before KSUID epoch (2014-05-13)');
}
// Generate 16 random bytes for payload
$payload = $this->randomGenerator->bytes(Ksuid::PAYLOAD_BYTES);
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**
* Generate a KSUID at a specific DateTime
*/
public function generateAtDateTime(DateTimeImmutable $dateTime): Ksuid
{
return $this->generateAt($dateTime->getTimestamp());
}
/**
* Generate a KSUID for the past (useful for testing)
*/
public function generateInPast(int $secondsAgo): Ksuid
{
$timestamp = time() - $secondsAgo;
return $this->generateAt($timestamp);
}
/**
* Generate a batch of KSUIDs with the same timestamp
*/
public function generateBatch(int $count, ?int $timestamp = null): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 10000) {
throw new InvalidArgumentException('Batch size cannot exceed 10000');
}
$timestamp ??= time();
$ksuids = [];
$used = [];
while (count($ksuids) < $count) {
$payload = $this->randomGenerator->bytes(Ksuid::PAYLOAD_BYTES);
$payloadHex = bin2hex($payload);
// Ensure uniqueness within batch
if (! isset($used[$payloadHex])) {
$ksuids[] = Ksuid::fromTimestampAndPayload($timestamp, $payload);
$used[$payloadHex] = true;
}
}
return $ksuids;
}
/**
* Generate a sequence of KSUIDs with incrementing timestamps
*/
public function generateSequence(int $count, int $intervalSeconds = 1): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 1000) {
throw new InvalidArgumentException('Sequence size cannot exceed 1000');
}
if ($intervalSeconds < 0) {
throw new InvalidArgumentException('Interval must be non-negative');
}
$ksuids = [];
$timestamp = time();
for ($i = 0; $i < $count; $i++) {
$ksuids[] = $this->generateAt($timestamp + ($i * $intervalSeconds));
}
return $ksuids;
}
/**
* Generate a KSUID with specific prefix in payload (for testing/debugging)
*/
public function generateWithPrefix(string $prefix): Ksuid
{
$maxPrefixLength = Ksuid::PAYLOAD_BYTES - 1; // Reserve at least 1 byte for randomness
if (strlen($prefix) > $maxPrefixLength) {
throw new InvalidArgumentException("Prefix cannot exceed {$maxPrefixLength} bytes");
}
$remainingBytes = Ksuid::PAYLOAD_BYTES - strlen($prefix);
$randomSuffix = $this->randomGenerator->bytes($remainingBytes);
$payload = $prefix . $randomSuffix;
return Ksuid::fromTimestampAndPayload(time(), $payload);
}
/**
* Parse a KSUID string and validate it
*/
public function parse(string $ksuidString): Ksuid
{
return Ksuid::fromString($ksuidString);
}
/**
* Validate if a string is a valid KSUID
*/
public function isValid(string $value): bool
{
if (strlen($value) !== Ksuid::ENCODED_LENGTH) {
return false;
}
try {
Ksuid::fromString($value);
return true;
} catch (InvalidArgumentException) {
return false;
}
}
/**
* Get the minimum possible KSUID for a timestamp
*/
public function getMinForTimestamp(int $timestamp): Ksuid
{
$payload = str_repeat("\0", Ksuid::PAYLOAD_BYTES);
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**
* Get the maximum possible KSUID for a timestamp
*/
public function getMaxForTimestamp(int $timestamp): Ksuid
{
$payload = str_repeat("\xFF", Ksuid::PAYLOAD_BYTES);
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**@return array{min: \App\Framework\Ksuid\Ksuid, max: \App\Framework\Ksuid\Ksuid}
* Generate KSUIDs for a time range (useful for queries)
*/
public function generateTimeRange(int $startTimestamp, int $endTimestamp): array
{
if ($startTimestamp >= $endTimestamp) {
throw new InvalidArgumentException('Start timestamp must be before end timestamp');
}
return [
'min' => $this->getMinForTimestamp($startTimestamp),
'max' => $this->getMaxForTimestamp($endTimestamp),
];
}
/**
* Create factory method
*/
public static function create(RandomGenerator $randomGenerator): self
{
return new self($randomGenerator);
}
}