Skip to content
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

[5.6] Potential race condition with model factory after callbacks #23621

Closed
Indemnity83 opened this issue Mar 19, 2018 · 7 comments
Closed

[5.6] Potential race condition with model factory after callbacks #23621

Indemnity83 opened this issue Mar 19, 2018 · 7 comments

Comments

@Indemnity83
Copy link
Contributor

Indemnity83 commented Mar 19, 2018

  • Laravel Version: 5.6.12
  • PHP Version: 7.1.14
  • Database Driver & Version: sqlite in memory (testing)

Description:

It looks like I introduced an unexpected bug/race condition with the behavior of after callbacks in PR #23495. In that the model returned can become out of sync with the database state that was created inside the factory()->create() call.

Steps To Reproduce:

Assuming you have users and teams:

class User extends Model
{
    public function team()
    {
        return $this->belongsToMany(Team::class);
    }
}

class Team extends Model
{
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

Example after callback for a user, who must be a member of a team to be a valid user:

$factory->define(App\User::class, function (Faker $faker) {
    return [
        // valid attributes ...
    ];
});

$factory->afterCreating(\App\User::class, function (\App\User $user, Faker $faker) {
    $team = factory(\App\Team::class)->create();
    $team->users()->attach($user);
});

In a test, the first test assertion will unexpectedly fail sometimes, but the second (after calling fresh()) will always pass.

$user = factory(User::class)->create();

$this->assertCount(1, count($user->teams));
$this->assertCount(1, count($user->fresh()->teams));
@Indemnity83
Copy link
Contributor Author

Indemnity83 commented Mar 19, 2018

False alarm, after simplifying my particular code for the example above it looks like its working, must just be a bug in my slightly more complex code.

@Indemnity83
Copy link
Contributor Author

Indemnity83 commented Mar 19, 2018

Re-opening ... looks like this might be a race condition. I'm getting varied results even when just re-running the same tests repeatedly.

I'm not sure if I can put together a test that is 100% reproducible though. Does this make sense to others that a race could be happening here?

I'm running tests with in-memory sqlite.

@Indemnity83 Indemnity83 reopened this Mar 19, 2018
@Indemnity83 Indemnity83 changed the title [5.6] Model factory after callbacks can return state that is out of sync with db [5.6] Potential race condition with model factory after callbacks Mar 20, 2018
@unstoppablecarl
Copy link
Contributor

It might be a sqlite quirk can you try mysql?

@ChrGriffin
Copy link

ChrGriffin commented Jun 2, 2019

I've just encountered an issue myself that seems very similar.

Consider the following test:

$language = factory(Language::class)->create();
$story = factory(Story::class)->create();
factory(Story::class)->create();
factory(Clip::class)->create([
    'story_id' => $story->id,
    'language_id' => $language->id]
);

$stories = Story::notEmpty()->get();
$this->assertFalse($stories->isEmpty());
foreach($stories as $story) {
    $this->assertFalse($story->clips->isEmpty());
}

The intent of the test is fairly self-evident. I've written a custom scope called scopeNotEmpty(), that only returns Stories (Categories) that have media clips attached.

public function scopeNotEmpty($query)
{
    return $query->whereHas('clips');
}

This test fails or passes randomly, on the assertion: $this->assertFalse($stories->isEmpty());. Randomly and without apparent cause, sometimes there are Stories in the database, and sometimes there aren't.

I'm running my tests on an in-memory SQLite database, just like @Indemnity83. Notably, I have an entire test suite that runs predictably. This is the first time I've encountered this behaviour. Unlike @Indemnity83, I do not have an existing entity that I can call fresh() on to get a fresh copy out of the database.

PS: As a truly blunt debug, I tried adding sleep(5) before the assertion, in case it was somehow a race condition with SQLite not writing the data fast enough. No change, still randomly fails or succeeds.

PPS: Despite the random behaviour of my custom scope, Story::all() returns consistent, predictable results. So the problem must be in my scope, somehow (that one line doesn't seem to leave a lot of room for error). I'm not ruling out that it's unrelated to a race condition in the factories, since the Clip, which is central to the concept of the scope, is created in a factory.

PPPS: After switching from using the factory to just using Clip::create(), the behaviour was immediately solved. Consistent, predictable results every time. I have to conclude the issue is with using the Factory.

@cbj4074
Copy link
Contributor

cbj4074 commented Jan 21, 2020

@tillkruss Why was this closed?

@edwinvdpol
Copy link

edwinvdpol commented Apr 9, 2020

Same problem here. I have to refresh() the model after creating and afterCreating is ran.
When I do a GET request afterwards without the refresh, it works.
When I do a PUT request afterwards without the refresh, it does not work.
When I do a PUT request afterwards with the refresh, it does work.

@TwilightDuck
Copy link

I know the issue is closed, but I still wanted to mention that I'm getting this behaviour now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants