Skip to content

Commit

Permalink
Basic working id resolver implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
inxilpro committed Aug 21, 2024
1 parent 78d223e commit 671d750
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 8 deletions.
4 changes: 3 additions & 1 deletion config/bits.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@
|
| Bits addresses this with a special lambda ID resolver, which assigns
| and locks IDs via your cache.
|
| Options: true, false, or "autodetect"
*/

'lambda' => (bool) env('BITS_LAMBDA', false),
'lambda' => env('BITS_LAMBDA', 'autodetect'),

/*
|--------------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions src/Config/WorkerIds.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Glhd\Bits\Config;

use ArrayAccess;
use BadMethodCallException;
use LogicException;

class WorkerIds implements ArrayAccess
Expand All @@ -20,6 +21,15 @@ public function first(): int
return $this->ids[0];
}

public function second(): int
{
if (! isset($this->ids[1])) {
throw new BadMethodCallException('No second ID configured.');
}

return $this->ids[1];
}

public function offsetExists(mixed $offset): bool
{
return isset($this->ids[$offset]);
Expand Down
2 changes: 1 addition & 1 deletion src/Contracts/ResolvesIds.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

interface ResolvesIds
{
public function get(): WorkerIds;
public function get(...$lengths): WorkerIds;
}
68 changes: 68 additions & 0 deletions src/IdResolvers/CacheResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Glhd\Bits\IdResolvers;

use Illuminate\Cache\CacheManager;
use Illuminate\Support\DateFactory;
use RuntimeException;

class CacheResolver extends IdResolver
{
protected ?int $value = null;

public function __construct(
protected CacheManager $cache,
protected DateFactory $dates,
protected int $max = 0b1111111111,
) {
}

protected function value(): int
{
return $this->value ??= $this->acquire();
}

protected function acquire(): int
{
// First we acquire a lock to ensure no other process is reserving an ID. Then we
// get the current list of reserved IDs, and either find one that hasn't been
// reserved, or one where the reservation is expired.

return $this->cache->lock('glhd-bits-ids:lock')
->block(5, function() {
$reserved = $this->cache->get('glhd-bits-ids:reserved', fn() => []);

if ($id = $this->firstAvailable($reserved) ?? $this->findExpired($reserved)) {
$reserved[$id] = $this->dates->now()->addHour()->unix();
$this->cache->forever('glhd-bits-ids:reserved', $reserved);
return $id;
}

throw new RuntimeException('Unable to acquire a unique worker ID.');
});
}

protected function firstAvailable(array $reserved): ?int
{
for ($id = 0; $id <= $this->max; $id++) {
if (! isset($reserved[$id])) {
return $id;
}
}

return null;
}

protected function findExpired(array $reserved): ?int
{
$now = $this->dates->now()->unix();

[$_, $id] = collect($reserved)
->filter(fn($expiration) => $expiration <= $now)
->reduce(function($carry, $expiration, $id) {
return $expiration < $carry[0] ? [$expiration, $id] : $carry;
}, [PHP_INT_MAX, null]);

return $id;
}
}
25 changes: 25 additions & 0 deletions src/IdResolvers/IdResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Glhd\Bits\IdResolvers;

use Glhd\Bits\Config\WorkerIds;
use Glhd\Bits\Contracts\ResolvesIds;

abstract class IdResolver implements ResolvesIds
{
abstract protected function value(): int;

public function get(...$lengths): WorkerIds
{
$value = $this->value();
$ids = [];

foreach (array_reverse($lengths) as $length) {
$bitmask = (1 << $length) - 1;
array_unshift($ids, $value & $bitmask);
$value = $value >> $length;
}

return new WorkerIds(...$ids);
}
}
2 changes: 1 addition & 1 deletion src/IdResolvers/StaticResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public function __construct(int ...$ids)
$this->ids = new WorkerIds(...$ids);
}

public function get(): WorkerIds
public function get(...$lengths): WorkerIds
{
return $this->ids;
}
Expand Down
59 changes: 54 additions & 5 deletions src/Support/BitsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
use Glhd\Bits\Contracts\MakesBits;
use Glhd\Bits\Contracts\MakesSnowflakes;
use Glhd\Bits\Contracts\MakesSonyflakes;
use Glhd\Bits\Contracts\ResolvesIds;
use Glhd\Bits\Contracts\ResolvesSequences;
use Glhd\Bits\Factories\SnowflakeFactory;
use Glhd\Bits\Factories\SonyflakeFactory;
use Glhd\Bits\IdResolvers\CacheResolver;
use Glhd\Bits\IdResolvers\StaticResolver;
use Glhd\Bits\SequenceResolvers\CacheSequenceResolver;
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Schema\Blueprint;
Expand All @@ -34,14 +38,59 @@ public function register()
$this->app->singleton(SnowflakesConfig::class);
$this->app->singleton(SonyflakesConfig::class);

$this->app->singleton(CacheResolver::class, function(Container $container) {
$config = $container->make(Repository::class);
$cache = $container->make(CacheManager::class);
$dates = $container->make(DateFactory::class);

$format = $config->get('bits.format', 'snowflake');

return match ($format) {
'snowflake', 'snowflakes' => new CacheResolver($cache, $dates, 0b1111111111),
'sonyflake', 'sonyflakes' => new CacheResolver($cache, $dates, 0b1111111111111111),
default => value(fn() => throw new InvalidArgumentException("Unknown bits format: '{$format}'")),
};
});

$this->app->singleton(StaticResolver::class, function(Container $container) {
$config = $container->make(Repository::class);
$format = $config->get('bits.format', 'snowflake');

return match ($format) {
'snowflake', 'snowflakes' => new StaticResolver(
$config->get('bits.datacenter_id') ?? random_int(0, 0b11111),
$config->get('bits.worker_id') ?? $this->generateWorkerId(0b11111)
),
'sonyflake', 'sonyflakes' => new StaticResolver(
$config->get('bits.worker_id') ?? $this->generateWorkerId(0b1111111111111111)
),
default => value(fn() => throw new InvalidArgumentException("Unknown bits format: '{$format}'")),
};
});

$this->app->singleton(ResolvesIds::class, function(Container $container) {
$config = $container->make(Repository::class);

$lambda = match ($config->get('bits.lambda', 'autodetect')) {
true, 1, '1' => true,
'autodetect' => ! empty(getenv('AWS_LAMBDA_FUNCTION_NAME')),
default => false,
};

return $lambda
? $container->make(CacheResolver::class)
: $container->make(StaticResolver::class);
});

$this->app->singleton(MakesSnowflakes::class, function(Container $container) {
$config = $container->make(Repository::class);
$dates = $container->make(DateFactory::class);
$ids = $container->make(ResolvesIds::class);

return new SnowflakeFactory(
epoch: $dates->parse($config->get('bits.epoch', '2023-01-01'), 'UTC')->startOfDay(),
datacenter_id: $config->get('bits.datacenter_id') ?? random_int(0, 31),
worker_id: $config->get('bits.worker_id') ?? $this->generateWorkerId(31),
datacenter_id: $ids->get(5, 5)->first(),
worker_id: $ids->get(5, 5)->second(),
config: $container->make(SnowflakesConfig::class),
sequence: $container->make(ResolvesSequences::class),
);
Expand All @@ -50,18 +99,18 @@ public function register()
$this->app->singleton(MakesSonyflakes::class, function(Container $container) {
$config = $container->make(Repository::class);
$dates = $container->make(DateFactory::class);
$ids = $container->make(ResolvesIds::class);

return new SonyflakeFactory(
epoch: $dates->parse($config->get('bits.epoch', '2023-01-01'), 'UTC')->startOfDay(),
machine_id: $config->get('bits.worker_id') ?? $this->generateWorkerId(65535),
machine_id: $ids->get(16)->first(),
config: $container->make(SonyflakesConfig::class),
sequence: $container->make(ResolvesSequences::class),
);
});

$this->app->singleton(MakesBits::class, function(Container $container) {
$format = $container->make(Repository::class)
->get('bits.format', 'snowflake');
$format = $container->make(Repository::class)->get('bits.format', 'snowflake');

return match ($format) {
'snowflake', 'snowflakes' => $container->make(MakesSnowflakes::class),
Expand Down

0 comments on commit 671d750

Please sign in to comment.