/**
  * Get a non running message from the queue.
  *
  * @param array $query in same format as \MongoDB\Collection::find() where top level fields do not contain operators.
  *                     Lower level fields can however. eg: valid {a: {$gt: 1}, "b.c": 3},
  *                     invalid {$and: [{...}, {...}]}
  * @param int $runningResetDuration second duration the message can stay unacked before it resets and can be
  *                                  retreived again.
  * @param int $waitDurationInMillis millisecond duration to wait for a message.
  * @param int $pollDurationInMillis millisecond duration to wait between polls.
  *
  * @return array|null the message or null if one is not found
  *
  * @throws \InvalidArgumentException $runningResetDuration, $waitDurationInMillis or $pollDurationInMillis was not
  *                                   an int
  * @throws \InvalidArgumentException key in $query was not a string
  */
 public function get(array $query, $runningResetDuration, $waitDurationInMillis = 3000, $pollDurationInMillis = 200)
 {
     if (!is_int($runningResetDuration)) {
         throw new \InvalidArgumentException('$runningResetDuration was not an int');
     }
     if (!is_int($waitDurationInMillis)) {
         throw new \InvalidArgumentException('$waitDurationInMillis was not an int');
     }
     if (!is_int($pollDurationInMillis)) {
         throw new \InvalidArgumentException('$pollDurationInMillis was not an int');
     }
     if ($pollDurationInMillis < 0) {
         $pollDurationInMillis = 0;
     }
     //reset stuck messages
     $this->collection->updateMany(['running' => true, 'resetTimestamp' => ['$lte' => new \MongoDB\BSON\UTCDateTime((int) (microtime(true) * 1000))]], ['$set' => ['running' => false]]);
     $completeQuery = ['running' => false];
     foreach ($query as $key => $value) {
         if (!is_string($key)) {
             throw new \InvalidArgumentException('key in $query was not a string');
         }
         $completeQuery["payload.{$key}"] = $value;
     }
     $completeQuery['earliestGet'] = ['$lte' => new \MongoDB\BSON\UTCDateTime((int) (microtime(true) * 1000))];
     $resetTimestamp = time() + $runningResetDuration;
     //ints overflow to floats
     if (!is_int($resetTimestamp)) {
         $resetTimestamp = $runningResetDuration > 0 ? self::MONGO_INT32_MAX : 0;
     }
     $resetTimestamp = min(max(0, $resetTimestamp * 1000), self::MONGO_INT32_MAX);
     $update = ['$set' => ['resetTimestamp' => new \MongoDB\BSON\UTCDateTime($resetTimestamp), 'running' => true]];
     $options = ['sort' => ['priority' => 1, 'created' => 1]];
     //ints overflow to floats, should be fine
     $end = microtime(true) + $waitDurationInMillis / 1000.0;
     $sleepTime = $pollDurationInMillis * 1000;
     //ints overflow to floats and already checked $pollDurationInMillis was positive
     if (!is_int($sleepTime)) {
         //ignore since testing a giant sleep takes too long
         //@codeCoverageIgnoreStart
         $sleepTime = PHP_INT_MAX;
     }
     //@codeCoverageIgnoreEnd
     while (true) {
         $message = $this->collection->findOneAndUpdate($completeQuery, $update, $options);
         //checking if _id exist because findAndModify doesnt seem to return null when it can't match the query on
         //older mongo extension
         if ($message !== null && array_key_exists('_id', $message)) {
             // findOneAndUpdate does not correctly return result according to typeMap options so just refetch.
             $message = $this->collection->findOne(['_id' => $message->_id]);
             //id on left of union operator so a possible id in payload doesnt wipe it out the generated one
             return ['id' => $message['_id']] + (array) $message['payload'];
         }
         if (microtime(true) >= $end) {
             return null;
         }
         usleep($sleepTime);
     }
     //ignore since always return from the function from the while loop
     //@codeCoverageIgnoreStart
 }
 public function removeProperty($property)
 {
     CodeGuard::checkTypeAndThrow($property, 'string');
     $filter = array($property => array('$exists' => true));
     $updateCommand = array('$unset' => array($property => true));
     $result = $this->_collection->updateMany($filter, $updateCommand);
     return $result->getModifiedCount();
 }