-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: State Reconstructor #160
base: main
Are you sure you want to change the base?
Changes from all commits
7c246a4
79e69d4
5c43a99
037c450
93582b5
ec6082f
25dbc6c
1989b9c
712463f
64a0819
72188a5
129b031
064fb3c
f87eb56
1d18e81
4edd585
9609c9f
a04e552
21283eb
07394a1
fe01611
91f3d87
fe66df1
5cd6c45
bf8ed4e
3ea993a
8569f3e
7c3b226
789aa40
399288c
a90c509
f266dbd
00d713f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<?php | ||
|
||
namespace Thunk\Verbs\Lifecycle; | ||
|
||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Support\Collection; | ||
use Thunk\Verbs\Models\VerbStateEvent; | ||
use Thunk\Verbs\State; | ||
use Thunk\Verbs\Support\StateIdentity; | ||
|
||
class AggregateStateSummary | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This name is… obtuse. But the basic point of this class is to find ALL the States and Events that are related to a given set of starting States. We need this because if we're reconstituting state, and it relies on some other related state, we need that state to be in sync. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. class FindAllTheThings
{
public static function leeroyyyyyyJenkins(State ...$states): static
} |
||
{ | ||
public static function summarize(State ...$states): static | ||
{ | ||
$summary = new static( | ||
original_states: Collection::make($states), | ||
related_event_ids: new Collection, | ||
related_states: Collection::make($states)->map(StateIdentity::from(...)), | ||
); | ||
|
||
return $summary->discover(); | ||
} | ||
|
||
/** | ||
* @param Collection<int, State> $original_states | ||
* @param Collection<int, int> $related_event_ids | ||
* @param Collection<int, StateIdentity> $related_states | ||
*/ | ||
public function __construct( | ||
public Collection $original_states = new Collection, | ||
public Collection $related_event_ids = new Collection, | ||
public Collection $related_states = new Collection, | ||
) {} | ||
|
||
protected function discover(): static | ||
{ | ||
$this->discoverNewEventIds(); | ||
|
||
do { | ||
$continue = $this->discoverNewStates() && $this->discoverNewEventIds(); | ||
} while ($continue); | ||
|
||
$this->related_event_ids = $this->related_event_ids->sort(); | ||
|
||
return $this; | ||
} | ||
|
||
protected function discoverNewEventIds(): bool | ||
{ | ||
$new_event_ids = VerbStateEvent::query() | ||
->distinct() | ||
->select('event_id') | ||
->whereNotIn('event_id', $this->related_event_ids) | ||
->where(fn (Builder $query) => $this->related_states->each( | ||
fn ($state) => $query->orWhere(fn (Builder $query) => $this->addConstraint($state, $query))) | ||
) | ||
->toBase() | ||
->pluck('event_id'); | ||
|
||
$this->related_event_ids = $this->related_event_ids->merge($new_event_ids); | ||
|
||
return $new_event_ids->isNotEmpty(); | ||
} | ||
|
||
protected function discoverNewStates(): bool | ||
{ | ||
$discovered_states = VerbStateEvent::query() | ||
->orderBy('id') | ||
->distinct() | ||
->select(['state_id', 'state_type']) | ||
->whereIn('event_id', $this->related_event_ids) | ||
->where(fn (Builder $query) => $this->related_states->each( | ||
fn ($state) => $query->whereNot(fn (Builder $query) => $this->addConstraint($state, $query))) | ||
) | ||
->toBase() | ||
->chunkMap(StateIdentity::from(...)); | ||
|
||
$this->related_states = $this->related_states->merge($discovered_states); | ||
|
||
return $discovered_states->isNotEmpty(); | ||
} | ||
|
||
protected function addConstraint(StateIdentity $state, Builder $query): Builder | ||
{ | ||
$query->where('state_type', '=', $state->state_type); | ||
$query->where('state_id', '=', $state->state_id); | ||
|
||
return $query; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
namespace Thunk\Verbs\Lifecycle; | ||
|
||
use Glhd\Bits\Bits; | ||
use Ramsey\Uuid\UuidInterface; | ||
use Symfony\Component\Uid\AbstractUid; | ||
use Thunk\Verbs\Contracts\StoresSnapshots; | ||
use Thunk\Verbs\State; | ||
use Thunk\Verbs\Support\StateCollection; | ||
|
||
class NullSnapshotStore implements StoresSnapshots | ||
{ | ||
public function load(Bits|UuidInterface|AbstractUid|iterable|int|string $id, string $type): State|StateCollection|null | ||
{ | ||
return null; | ||
} | ||
|
||
public function loadSingleton(string $type): ?State | ||
{ | ||
return null; | ||
} | ||
|
||
public function write(array $states): bool | ||
{ | ||
return true; | ||
} | ||
|
||
public function delete(Bits|UuidInterface|AbstractUid|int|string ...$ids): bool | ||
{ | ||
return true; | ||
} | ||
|
||
public function reset(): bool | ||
{ | ||
return true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,13 +11,17 @@ | |
use Thunk\Verbs\Contracts\StoresSnapshots; | ||
use Thunk\Verbs\Event; | ||
use Thunk\Verbs\Facades\Id; | ||
use Thunk\Verbs\Models\VerbStateEvent; | ||
use Thunk\Verbs\State; | ||
use Thunk\Verbs\Support\EventStateRegistry; | ||
use Thunk\Verbs\Support\StateCollection; | ||
use Thunk\Verbs\Support\StateInstanceCache; | ||
use UnexpectedValueException; | ||
|
||
class StateManager | ||
{ | ||
protected bool $is_reconstituting = false; | ||
|
||
protected bool $is_replaying = false; | ||
|
||
public function __construct( | ||
|
@@ -42,6 +46,12 @@ public function register(State $state): State | |
*/ | ||
public function load(Bits|UuidInterface|AbstractUid|iterable|int|string $id, string $type): StateCollection|State | ||
{ | ||
// FIXME: This was not written to support loading multiple states | ||
// $summary = $this->events->summarize($state); | ||
// if ($summary->out_of_sync) { | ||
// $this->snapshots->delete(...$summary->related_state_ids); | ||
// } | ||
|
||
return is_iterable($id) | ||
? $this->loadMany($id, $type) | ||
: $this->loadOne($id, $type); | ||
|
@@ -110,6 +120,8 @@ public function setReplaying(bool $replaying): static | |
public function reset(bool $include_storage = false): static | ||
{ | ||
$this->states->reset(); | ||
app(EventStateRegistry::class)->reset(); | ||
|
||
$this->is_replaying = false; | ||
|
||
if ($include_storage) { | ||
|
@@ -154,7 +166,7 @@ protected function loadOne(Bits|UuidInterface|AbstractUid|int|string $id, string | |
$this->remember($state); | ||
$this->reconstitute($state); | ||
|
||
return $state; | ||
return $this->states->get($key); // FIXME | ||
} | ||
|
||
/** @param class-string<State> $type */ | ||
|
@@ -181,30 +193,75 @@ protected function loadMany(iterable $ids, string $type): StateCollection | |
|
||
// At this point, all the states should be in our cache, so we can just load everything | ||
return StateCollection::make( | ||
$ids->map(fn ($id) => $this->states->get($this->key($id, $type))) | ||
$ids->map(fn ($id) => $this->states->get($this->key($id, $type))), | ||
); | ||
} | ||
|
||
protected function reconstitute(State $state): static | ||
{ | ||
// When we're replaying, the Broker is in charge of applying the correct events | ||
// to the State, so we only need to do it *outside* of replays. | ||
if (! $this->is_replaying) { | ||
$this->events | ||
->read(state: $state, after_id: $state->last_event_id) | ||
->each(fn (Event $event) => $this->dispatcher->apply($event)); | ||
|
||
// It's possible for an event to mutate state out of order when reconstituting, | ||
// so as a precaution, we'll clear all other states from the store and reload | ||
// them from snapshots as needed in the rest of the request. | ||
// FIXME: We still need to figure this out | ||
// $this->states->reset(); | ||
// $this->remember($state); | ||
// FIXME: Only run this if the state is out of date | ||
if (! $this->needsReconstituting($state)) { | ||
// dump('skipping: everything in sync'); | ||
return $this; | ||
} | ||
|
||
if (! $this->is_replaying && ! $this->is_reconstituting) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be useful to combine |
||
$real_registry = app(EventStateRegistry::class); | ||
|
||
try { | ||
$this->is_reconstituting = true; | ||
|
||
$summary = $this->events->summarize($state); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably should be able to take multiple states |
||
|
||
[$temp_manager] = $this->bindNewEmptyStateManager(); | ||
|
||
$this->events | ||
->get($summary->related_event_ids) | ||
->each($this->dispatcher->apply(...)); | ||
|
||
foreach ($temp_manager->states->all() as $key => $state) { | ||
$this->states->put($key, $state); | ||
} | ||
|
||
} finally { | ||
$this->is_reconstituting = false; | ||
|
||
app()->instance(StateManager::class, $this); | ||
app()->instance(EventStateRegistry::class, $real_registry); | ||
} | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
protected function needsReconstituting(State $state): bool | ||
{ | ||
$max_id = VerbStateEvent::query() | ||
->where('state_id', $state->id) | ||
->where('state_type', $state::class) | ||
->max('event_id'); | ||
|
||
return $max_id !== $state->last_event_id; | ||
} | ||
|
||
protected function bindNewEmptyStateManager() | ||
{ | ||
$temp_manager = new StateManager( | ||
dispatcher: $this->dispatcher, | ||
snapshots: new NullSnapshotStore, | ||
events: $this->events, | ||
states: new StateInstanceCache, | ||
); | ||
$temp_manager->is_reconstituting = true; // FIXME | ||
|
||
$temp_registry = new EventStateRegistry($temp_manager); | ||
|
||
app()->instance(StateManager::class, $temp_manager); | ||
app()->instance(EventStateRegistry::class, $temp_registry); | ||
|
||
return [$temp_manager, $temp_registry]; | ||
} | ||
|
||
protected function remember(State $state): State | ||
{ | ||
$key = $this->key($state->id, $state::class); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure this needs to be on the
StoresEvents
interface. I think maybe it needs access to some store data, but it seems a little odd for it to live here.