graphql-php: Using with dataloader-php throws "Could not resolve promise" Exception

I have three types

type Questionnaire {
  id: Int!
  blocks: [Int]!
}

type Block {
  id: Int!
  wrappers: [Wrapper]!
}

type Wrapper {
  id: Int!
  questions: [Question]!
}

type Question {
  id: Int!
}

type Query {
  questionnaire(questionnaireId: Int!): Questionnaire
  wrapper(wrapperId: Int!): Wrapper
}

Every field that returns a list uses a DataLoader (like in https://github.com/overblog/dataloader-php#using-with-webonyxgraphql). Each DataLoader is identical, despite the tables from which the dataloaders fetch their data.

The following query works fine:

query questionnaire {
  questionnaire(questionnaireId: 1) { # no dataloader
    blocks { # resolver uses dataloader
      wrappers { # resolvers uses dataloader
        id
      }
    }
  }
}

However going one level deeper results in an exception.

query questionnaire {
  questionnaire(questionnaireId: 1) { # no dataloader
    blocks { # resolver uses dataloader
      wrappers { # resolvers uses dataloader
        id
        questions { # resolver uses dataloader
          id
        }
      }
    }
  }
}

InvariantViolation in SyncPromiseAdapter.php line 151:
Could not resolve promise

When I log the status of the promise I get pending.

According to the code this should not happen. https://github.com/webonyx/graphql-php/blob/master/src/Executor/Promise/Adapter/SyncPromiseAdapter.php#L132

When I use the following query (only 2 dataloader) everything is behaving like excpected:

query wrapper {
  wrapper(wrapperId: 1) { # resolvers uses dataloader
    id
    questions { # resolver uses dataloader
      id
    }
  }
}

Edit: This also happens in this resolver function where I chain multiple promises (I am using laravel):

public function resolveIsValidField($questionnaire, $args, $context) {
  $questionnaireId = is_array($questionnaire) ? $questionnaire['id'] : $questionnaire->id;
  /** @var BlockRepositoryInterface $blockRepository */
  $blockRepository = $context['repositories']['blocks'];
  /** @var WrapperRepositoryInterface $wrapperRepository */
  $wrapperRepository = $context['repositories']['wrappers'];
  /** @var QuestionRepositoryInterface $questionRepository */
  $questionRepository = $context['repositories']['questions'];

  $blocks = null;
  $wrappers = null;

  return $blockRepository->findWhereQuestionnaireId($questionnaireId)
    ->then(function (Collection $blockRecords) use (&$blocks, $wrapperRepository) {
      $blocks = $blockRecords;
      $bockIds = $blocks->map(function (Block $block) {
        return $block->id;
      })->toArray();

      return $wrapperRepository->getWhereBlockIds($bockIds);
    })
    ->then(function ($wrapperRecords) use (&$wrappers, $questionRepository) {
      $wrappers = collect($wrapperRecords)->flatten();
      $wrapperIds = $wrappers->pluck('id');
      return $questionRepository->getWhereWrapperIds($wrapperIds->toArray());
    })
    ->then(function ($questionRecords) use (&$blocks, &$wrappers) {
      $questions = collect($questionRecords)->flatten();

      return isQuestionnaireValid($blocks, $wrappers, $questions);
    });
}

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 18 (11 by maintainers)

Most upvoted comments

I’ll submit it, thank you @vladar 😉

Works for me, pragmatically %) Can anyone prepare PR for this? I’ll merge it.

I forked your repo and replaced DataLoaders with regular GraphQL\Deferred calls. It works as expected.

I guess the bug is somewhere on DataLoader side.

@mcg-web Here is my reproduction repository: https://github.com/n1ru4l/laravel-graphql-dataloader-php-exception-reproduction

The relevant files are the Service Provider (app/Providers/AppServiceProvider.php), the types and queries app/GraphQL/* and the DataLoaders app/DataLoader/*.php

You will not need any database setup just use php artisan serve.

You can navigate to graphiql on /graphiql.

This query fails:

query type1 {
  type1 {
    __typename
    id
    items {
      __typename
      id
      items {
        __typename
        id
        items {
          __typename
          id
        }
      }
    }
  }

This query works:

query type1 {
  type1 {
    __typename
    id
    items {
      __typename
      id
    }
  }
}

Thanks! Merged and released 0.9.14

here a solution that works:

  • we add two hooks to GraphQL lib
// src/Executor/Promise/Adapter/SyncPromiseAdapter.php

    /**
     * Synchronously wait when promise completes
     *
     * @param Promise $promise
     * @return mixed
     */
    public function wait(Promise $promise)
    {
        $this->beforeWait($promise);

        $dfdQueue = Deferred::getQueue();
        $promiseQueue = SyncPromise::getQueue();

        while (
            $promise->adoptedPromise->state === SyncPromise::PENDING &&
            !($dfdQueue->isEmpty() && $promiseQueue->isEmpty())
        ) {
            Deferred::runQueue();
            SyncPromise::runQueue();
            $this->onWait($promise);
        }

        /** @var SyncPromise $syncPromise */
        $syncPromise = $promise->adoptedPromise;

        if ($syncPromise->state === SyncPromise::FULFILLED) {
            return $syncPromise->result;
        } else if ($syncPromise->state === SyncPromise::REJECTED) {
            throw $syncPromise->result;
        }

        throw new InvariantViolation("Could not resolve promise");
    }

    /**
     * Execute just before starting to run promise completion
     *
     * @param Promise $promise
     */
    protected function beforeWait(Promise $promise)
    {
    }

    /**
     * While running promise completion
     *
     * @param Promise $promise
     */
    protected function onWait(Promise $promise)
    {
    }
  • we modify DataLoader custom implementation to do the work
    protected function beforeWait(Promise $promise)
    {
        DataLoader::await();
    }

    protected function onWait(Promise $promise)
    {
        DataLoader::await();
    }

@vladar tell me what do you think of this solution please?

@mcg-web If I add DataLoader::await() to queue progression, it works, e.g.:

    /**
     * Synchronously wait when promise completes
     *
     * @param Promise $promise
     * @return mixed
     */
    public function wait(Promise $promise)
    {
        $dfdQueue = Deferred::getQueue();
        $promiseQueue = SyncPromise::getQueue();

        while (
            $promise->adoptedPromise->state === SyncPromise::PENDING &&
            !($dfdQueue->isEmpty() && $promiseQueue->isEmpty())
        ) {
            Deferred::runQueue();
            SyncPromise::runQueue();
            DataLoader::await();
        }

        /** @var SyncPromise $syncPromise */
        $syncPromise = $promise->adoptedPromise;

        if ($syncPromise->state === SyncPromise::FULFILLED) {
            return $syncPromise->result;
        } else if ($syncPromise->state === SyncPromise::REJECTED) {
            throw $syncPromise->result;
        }

        throw new InvariantViolation("Could not resolve promise");
    }

So the whole queue must be progressed together. I guess you guys will have to write your own wait implementation to make it behave as expected.

Obviously, I am not 100% sure that this is enough to fix it (as I am not aware of DataLoader::await implementation details).

But let me know if I can make it easier for you somehow (e.g. by changing an interface of SyncPromiseAdapter or maybe changing how Executor calls wait).