Skip to content

Conversation

@tomchkk
Copy link
Contributor

@tomchkk tomchkk commented Feb 3, 2023

This PR removes the migration state handling initially included, modifying the initial MutexRelay to use Laravel's cache locking as opposed to the more complex MutexQueue implementation.

@tomchkk tomchkk requested a review from kubatek94 February 3, 2023 17:10
Copy link

@kubatek94 kubatek94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely changes Tom, negative code is the best :D

I have added a few small suggestions.

However I think my main concern is about the case when the '--mutex' option is not used, and we would like to fall back to standard Laravel migration command behaviour.

I appreciate the use of the Null design pattern and it seems like a sound design, however I am not sure if simply short circuiting and calling the parent handle method wouldn't be more appropriate. This would result in more closer behaviour to the original (however it may end up working in the future Laravel versions) and would truly mean that we don't really 'activate' our behaviour until that '--mutex' option is passed.

I also wonder about the handling of process signals in the original Migrate command, vs our extension without '--mutex' option and whether it will leave our extension truly 'inactive'.

Just some food for thought :)

$this->processor->terminate();
}

private function extendSignature(): void

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't shake the feeling, that while it may include a bit of duplication of the option name, it's much easier to mentally parse something like this:

        $this->signature = "$this->signature
                                { --mutex : Run a mutually exclusive migration }";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ported this from some work I'd done extending migrations on UK Bathrooms. I looked into it a bit further and think I've found something of a compromise. See what you think.


public function __construct(
private readonly Repository $cache,
private readonly int $ttl_seconds = 60

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would normally go with camel case, but I would also consider a bit more descriptive name:

Suggested change
private readonly int $ttl_seconds = 60
private readonly int $lockDurationSeconds = 60

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, not sure how that ended up like that 😊

return false;
}

return Str::contains($th->getMessage(), ['Base table or view new found', 'cache'], true);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more robust way would be to check the SQL error code - I think there should be a specific one for this error accessible somewhere in the exception properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated it, though it did require me to add an extended exception class, just so I could set a error code as a string. Still, however, I found that I needed to check for some text in the message, to be sure of the table name in question.

$provider = $this->cache->getStore();

return $this->queue->contains(fn ($item) => $item === $meta);
return $this->lock = $provider->lock(self::KEY . '.lock');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a big deal, because in the current form this scenario won't happen, however normally I think the first thing a method like that should do is check if $this->lock is already set, and return that instead, only assigning the lock property once. I would have an expectation like this, because the method assigns a single shared class property, rather than just returning $provider->lock() result.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% - I'd definitely code it like that ordinarily and think I didn't for the precise reason that it seemed like it wouldn't occur. Anyway, I added an early return if the instance already exists.

Comment on lines 31 to 32
Cache::store(Config::get('mutex-migrations.queue.store')),
Config::get('mutex-migrations.queue.ttl_seconds')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider renaming the config properties a little to fit with the changes

Suggested change
Cache::store(Config::get('mutex-migrations.queue.store')),
Config::get('mutex-migrations.queue.ttl_seconds')
Cache::store(Config::get('mutex-migrations.lock.store')),
Config::get('mutex-migrations.lock.ttl_seconds')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@tomchkk
Copy link
Contributor Author

tomchkk commented Feb 17, 2023

Lovely changes Tom, negative code is the best :D

I have added a few small suggestions.

However I think my main concern is about the case when the '--mutex' option is not used, and we would like to fall back to standard Laravel migration command behaviour.

I appreciate the use of the Null design pattern and it seems like a sound design, however I am not sure if simply short circuiting and calling the parent handle method wouldn't be more appropriate. This would result in more closer behaviour to the original (however it may end up working in the future Laravel versions) and would truly mean that we don't really 'activate' our behaviour until that '--mutex' option is passed.

I also wonder about the handling of process signals in the original Migrate command, vs our extension without '--mutex' option and whether it will leave our extension truly 'inactive'.

Just some food for thought :)

Thanks for the useful suggestions and comments. I've made updates based on the specific suggestions you made in order to check them off the list, as it were.

Re: keeping the two paths completely separate - I get what you're saying and have been looking into it, but I haven't immediately managed to find a satisfactory way of achieving that - at least while preserving the --mutex option. I'll have a think about it over the next week and have another look next Friday.

@tomchkk
Copy link
Contributor Author

tomchkk commented Feb 20, 2023

OK - I figured out a workable solution. You'll notice I left in (one of) the migration processing elements. I did attempt to move that into the new MutexMigrationCommand class itself, but I didn't really like the result or the prospect of having to test the process within the command. I also had to update the signal handling as it wasn't working and the docs have been changed since the original work. See what you think.

@tomchkk tomchkk requested a review from kubatek94 February 20, 2023 09:22
Copy link

@kubatek94 kubatek94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really happy with the changes @tomchkk - I think it's a pretty good solution in the end, which keeps the original implementation pretty much unchanged. Please take a look at further feedback that I think would be good to consider to make it more bullet proof.

sleep($wait++);

return $wait;
return $th->getCode() === '42S02' && Str::contains($th->getMessage(), 'cache_locks');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been thinking about it, and how it would be good for the table name to not be hardcoded, but instead use whatever the lock provider implementation is using. However, I started thinking, do we really need to do that?

Given that the throwable instance here is QueryException, we know that the lock implementation is backed with 'DatabaseStore', we know it failed to run a query, we know it failed when we tried to get a lock, and we know it failed because 'a table or view not found' - I think it's safe to assume it's due to the locks table not existing.

However, if you think searching for the table name is preferable, then I do believe we should read that table name from the config of the cache store, named by the 'mutex-migrations.lock.store'. That's because I see that in the source code of DatabaseStore the table name has default value set to 'cache_locks', but for example in GRJ codebase, the 'cache.stores.database.table' config value is 'cache' - and that's what would be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - totally got both points about a) not hardcoding the lock table name and b) whether we really need this check. I tried without it and felt that the result could be confusing because you're basically just told that you probably need to run migrations. It misses the specificity of the situation.

So I'd say we keep this and, to that end, I've updated it so that it tries to fetch the configured lock_table value, or uses a default.

Comment on lines 41 to 44
} catch (\Throwable $th) {
$this->components->error($th->getMessage());

return self::FAILURE;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would only handle specific exceptions this way (just printing their message). The behaviour of the base migrate command is to not catch them, so that they will end up within sentry and printed out to console - including all the detail which may be helpful sometimes. If e.g. you think handling the 'cache table not found' would be better like this, then perhaps we should catch that specific exception only, and let the rest fail as usual?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. I've updated it to only handle the now expected cache lock table not found exception.

@@ -0,0 +1,14 @@
<?php

namespace Netsells\LaravelMutexMigrations\Tests\Unit\Mutex\fixtures;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think following normal namespace conventions would be good here as well.

Suggested change
namespace Netsells\LaravelMutexMigrations\Tests\Unit\Mutex\fixtures;
namespace Netsells\LaravelMutexMigrations\Tests\Unit\Mutex\Fixtures;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯%

@tomchkk
Copy link
Contributor Author

tomchkk commented Feb 21, 2023

Thanks for the feedback @kubatek94 - will try to look on Friday, but feels like we're getting close now 😊

Btw, I learned about isolatable commands for the first time yesterday. I'm fairly sure this is a relatively recent addition to the documentation. Still my heart sank for a moment after spending time on this solution. However, looking at it - and the code - all it seems to do is to add a lock to prevent a command running elsewhere whereas this package will effectively allow migrations to be queued up. Thoughts?

@kubatek94
Copy link

Hey @tomchkk, yeah we are definitely close - it's just few new things I picked up on, but nothing major. Thanks for sharing about isolatable commands - today I learnt :D As you say, it seems that they have different behaviour than what we wanted to achieve here. We wanted to block our command until the other is done, whereas the isolatable interface would cause our migration command to 'succeed' straightaway if another instance is running, so not desirable. Which is great, as I had the same feeling you describe for a moment there haha.

@tomchkk tomchkk requested a review from kubatek94 March 3, 2023 15:03
Copy link

@kubatek94 kubatek94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done @tomchkk, great changes!

@tomchkk tomchkk merged commit eae7bde into main Mar 9, 2023
@tomchkk tomchkk deleted the feature/remove-migration-states branch March 9, 2023 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants