/** * @param string $sagaType * @param string $identity * @param array $associationValues * @param string $data * @return void */ public function update(string $sagaType, string $identity, array $associationValues, string $data) { if ($this->collection->count(['identity' => $identity]) == 0) { $this->insert($sagaType, $identity, $associationValues, $data); } else { $this->collection->findOneAndUpdate(['identity' => $identity, 'type' => $sagaType, 'associations' => $associationValues], ['$set' => ['serialized' => $data]]); } }
/** * Update a document and return it * * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php * @param array $query The query criteria to search for. * @param array $update The update criteria. * @param array $fields Optionally only return these fields. * @param array $options An array of options to apply, such as remove the match document from the DB and return it. * @return array Returns the original document, or the modified document when new is set. */ public function findAndModify(array $query, array $update = null, array $fields = null, array $options = []) { $query = TypeConverter::fromLegacy($query); try { if (isset($options['remove'])) { unset($options['remove']); $document = $this->collection->findOneAndDelete($query, $options); } else { $update = is_array($update) ? TypeConverter::fromLegacy($update) : []; if (isset($options['new'])) { $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER; unset($options['new']); } $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : []; $document = $this->collection->findOneAndUpdate($query, $update, $options); } } catch (\MongoDB\Driver\Exception\ConnectionException $e) { throw new MongoResultException($e->getMessage(), $e->getCode(), $e); } catch (\MongoDB\Driver\Exception\Exception $e) { ExceptionConverter::toLegacy($e, 'MongoResultException'); } if ($document) { $document = TypeConverter::toLegacy($document); } return $document; }
/** * Updates a document and returns it. * @param array $condition query condition * @param array $update update criteria * @param array $fields fields to be returned * @param array $options list of options in format: optionName => optionValue. * @return array|null the original document, or the modified document when $options['new'] is set. * @throws Exception on failure. * @see http://www.php.net/manual/en/mongocollection.findandmodify.php */ public function findAndModify($condition, $update, $options = []) { $token = "mongoyii\\" . $this->collection->getCollectionName() . ".findAndModify({\$condition: " . json_encode($condition) . ", \$update: " . json_encode($update) . ", \$options: " . json_encode($options) . " })"; Yii::trace($token, "mongoyii\\Collection"); if ($this->client->enableProfiling) { Yii::beginProfile($token, 'mongoyii\\Collection.findAndModify'); } try { if (isset($options['remove']) && $options['remove']) { $result = $this->collection->findOneAndDelete($condition, $options); } elseif (isset($options['update']) && $options['update']) { if (isset($options['upsert']) && $options['upsert']) { $result = $this->collection->findOneAndReplace($condition, $update, $options); } else { $result = $this->collection->findOneAndUpdate($condition, $update, $options); } } else { throw new Exception('Must enter a operation type for findAndModify'); } if ($this->client->enableProfiling) { Yii::endProfile($token, 'mongoyii\\Collection.findAndModify'); } return $result; } catch (\Exception $e) { if ($this->client->enableProfiling) { Yii::endProfile($token, 'mongoyii\\Collection.findAndModify'); } throw new Exception($e->getMessage(), (int) $e->getCode(), $e); } }
/** * 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 }
/** * {@inheritdoc} */ public function findOneAndUpdate($filter, $update, array $options = []) { $event = $this->startQueryLogging(__FUNCTION__, $filter, $update, $options); $result = parent::findOneAndUpdate($filter, $update, $options); $this->logger->logQuery($event); return $result; }
/** * Update a document and return it * * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php * @param array $query The query criteria to search for. * @param array $update The update criteria. * @param array $fields Optionally only return these fields. * @param array $options An array of options to apply, such as remove the match document from the DB and return it. * @return array Returns the original document, or the modified document when new is set. */ public function findAndModify(array $query, array $update = null, array $fields = null, array $options = []) { $query = TypeConverter::fromLegacy($query); if (isset($options['remove'])) { unset($options['remove']); $document = $this->collection->findOneAndDelete($query, $options); } else { $update = is_array($update) ? TypeConverter::fromLegacy($update) : []; if (isset($options['new'])) { $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER; unset($options['new']); } $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : []; $document = $this->collection->findOneAndUpdate($query, $update, $options); } if ($document) { $document = TypeConverter::toLegacy($document); } return $document; }
/** * Add to process registry. Adds based on $maxGlobalProcesses and $maxHostProcesses after a process registry cleaning. * * @param \MongoDB\Collection $collection the collection * @param string $id a unique id * @param int $minsBeforeExpire number of minutes before a process is considered expired. * @param int $maxGlobalProcesses max processes of an id allowed to run across all hosts. * @param int $maxHostProcesses max processes of an id allowed to run across a single host. * * @return boolean true if the process was added, false if not or there is too much concurrency at the moment. * * @throws \InvalidArgumentException if $id was not a string * @throws \InvalidArgumentException if $minsBeforeExpire was not an int * @throws \InvalidArgumentException if $maxGlobalProcesses was not an int * @throws \InvalidArgumentException if $maxHostProcesses was not an int */ public static function add(\MongoDB\Collection $collection, $id, $minsBeforeExpire = PHP_INT_MAX, $maxGlobalProcesses = 1, $maxHostProcesses = 1) { if (!is_string($id)) { throw new \InvalidArgumentException('$id was not a string'); } if (!is_int($minsBeforeExpire)) { throw new \InvalidArgumentException('$minsBeforeExpire was not an int'); } if (!is_int($maxGlobalProcesses)) { throw new \InvalidArgumentException('$maxGlobalProcesses was not an int'); } if (!is_int($maxHostProcesses)) { throw new \InvalidArgumentException('$maxHostProcesses was not an int'); } $thisHostName = self::_getEncodedHostname(); $thisPid = getmypid(); //loop in case the update fails its optimistic concurrency check for ($i = 0; $i < 5; ++$i) { $collection->findOneAndUpdate(['_id' => $id], ['$setOnInsert' => ['hosts' => [], 'version' => new \MongoDB\BSON\ObjectID()]], ['upsert' => true]); $existing = $collection->findOne(['_id' => $id], ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]); $replacement = $existing; $replacement['version'] = new \MongoDB\BSON\ObjectID(); //clean $replacement based on their pids and expire times foreach ($existing['hosts'] as $hostname => $pids) { foreach ($pids as $pid => $expires) { //our machine and not running //the task expired //our machine and pid is recycled (should rarely happen) if ($hostname === $thisHostName && !file_exists("/proc/{$pid}") || time() >= $expires->toDateTime()->getTimestamp() || $hostname === $thisHostName && $pid === $thisPid) { unset($replacement['hosts'][$hostname][$pid]); } } if (empty($replacement['hosts'][$hostname])) { unset($replacement['hosts'][$hostname]); } } $totalPidCount = 0; foreach ($replacement['hosts'] as $hostname => $pids) { $totalPidCount += count($pids); } $thisHostPids = array_key_exists($thisHostName, $replacement['hosts']) ? $replacement['hosts'][$thisHostName] : []; if ($totalPidCount >= $maxGlobalProcesses || count($thisHostPids) >= $maxHostProcesses) { return false; } // add our process $expireSecs = time() + $minsBeforeExpire * 60; if (!is_int($expireSecs)) { if ($minsBeforeExpire > 0) { $expireSecs = self::MONGO_INT32_MAX; } else { $expireSecs = 0; } } $thisHostPids[$thisPid] = new \MongoDB\BSON\UTCDateTime($expireSecs * 1000); $replacement['hosts'][$thisHostName] = $thisHostPids; $status = $collection->replaceOne(['_id' => $existing['_id'], 'version' => $existing['version']], $replacement, ['writeConcern' => new \MongoDB\Driver\WriteConcern(1, 100, true)]); if ($status->getMatchedCount() === 1) { return true; } //@codeCoverageIgnoreStart //hard to test the optimistic concurrency check } //too much concurrency at the moment, return false to signify not added. return false; //@codeCoverageIgnoreEnd }