/** * Adds an object to the persistent data store. * * @param \SmashPig\Core\DataStores\KeyedOpaqueStorableObject $obj * * @throws \SmashPig\Core\DataStores\DataStoreException if the message could not be stored. * @return null */ public function addObject(KeyedOpaqueStorableObject $obj) { $keys = $obj->getObjectKeys(); // FIXME magic string is magic if (!array_key_exists('correlationId', $keys)) { throw new DataStoreException("Required property correlationId was not exposed."); } $corrId = $keys['correlationId']; if (!isset($this->messages[$corrId])) { $this->messages[$corrId] = array(); } array_push($this->messages[$corrId], $obj); }
/** * Instantiates and runs a job defined by a queue message. Depends on * the base consumer's damaged message store functionality to either * divert messages or stop execution on bad message or job failure. * @param array $jobMessage * @throws \SmashPig\Core\DataStores\DataSerializationException */ function processMessage($jobMessage) { if (!isset($jobMessage['php-message-class'])) { throw new RuntimeException('Job message missing required key \'php-message-class\''); } // TODO: encapsulate the reconstitution step elsewhere. // FIXME It seems bad that these objects indiscriminately store // things as properties. The message is mingled with stuff like // php-message-class. Could collide. $className = $jobMessage['php-message-class']; Logger::info("Hydrating a message with class {$className}"); $jsonMessage = json_encode($jobMessage); Logger::debug("Job payload: {$jsonMessage}"); $jobObj = KeyedOpaqueStorableObject::fromJsonProxy($className, $jsonMessage); if ($jobObj instanceof Runnable) { Logger::info('Running job'); if (!$jobObj->execute()) { throw new RuntimeException('Job tells us that it did not successfully execute. ' . 'Sending to damaged message store.'); } } else { // We don't know how to run this job type. throw new RuntimeException(get_class($jobObj) . ' is not an instance of Runnable. ' . 'Could not execute and sending to damaged message store.'); } $this->successCount += 1; }
/** * Adds an object to the persistent data store. * * @param KeyedOpaqueStorableObject $obj * * @throws DataStoreException if the message could not be stored. * @return null */ public function addObject(KeyedOpaqueStorableObject $obj) { $keys = $obj->getObjectKeys(); $objFileName = $this->constructFileName(Context::get()->getContextId(), $keys, $this->insertCount++); $objFsPath = $this->basePath . '/objects/' . $objFileName; /* --- Create the root object file --- */ if (file_exists($objFsPath) || ($fptr = fopen($objFsPath, 'xb')) === false) { throw new DataStoreException("Could not add object to store! Fully qualified key '{$objFileName}' already exists!"); } fwrite($fptr, get_class($obj)); fwrite($fptr, "\n"); fwrite($fptr, $obj->toJson()); fclose($fptr); /* === Create the helper linking files === */ /* --- Class file first --- */ $this->addKeyedLinkingFile('class', get_class($obj), $objFileName, $objFsPath); /* --- Everything else --- */ foreach ($keys as $key => $value) { $this->addKeyedLinkingFile($key, $value, $objFileName, $objFsPath); } }
public function testConsume() { foreach (self::$messages as $msg) { $this->capture($msg['payload']); $jobQueue = $this->config->object('data-store/jobs-paypal'); $jobMessage = $jobQueue->pop(); $job = KeyedOpaqueStorableObject::fromJsonProxy($jobMessage['php-message-class'], json_encode($jobMessage)); $job->execute(); $queue = $this->config->object('data-store/' . $msg['type']); $queue->createTable($msg['type']); $message = $queue->pop(); if ($job->is_reject()) { $this->assertEmpty($message); } else { $this->assertNotEmpty($message); if (isset($message['contribution_tracking_id'])) { $this->assertEquals($message['contribution_tracking_id'], $message['order_id']); } } } }
public function testRecordCapture() { $verifiedQueue = BaseQueueConsumer::getQueue('verified'); $verifiedQueue->createTable('verified'); $capture = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Capture', file_get_contents(__DIR__ . '/../Data/capture.json')); $job = RecordCaptureJob::factory($capture); $this->assertTrue($job->execute()); $donorData = $this->pendingDatabase->fetchMessageByGatewayOrderId('adyen', $capture->merchantReference); $this->assertNull($donorData, 'RecordCaptureJob left donor data on pending queue'); $verifiedMessage = $verifiedQueue->pop(); $this->assertNotNull($verifiedMessage, 'RecordCaptureJob did not send verified message'); // can we use arraySubset yet? $sameKeys = array_intersect(array_keys($verifiedMessage), array_keys($this->pendingMessage)); foreach ($sameKeys as $key) { if ($key === 'gateway_txn_id') { $this->assertEquals($capture->originalReference, $verifiedMessage[$key], 'RecordCaptureJob should have set gateway_txn_id'); } else { $this->assertEquals($this->pendingMessage[$key], $verifiedMessage[$key], "Value of key {$key} mutated"); } } }
public function __construct() { parent::__construct(); $this->{'php-message-class'} = get_called_class(); }
/** * @expectedException \SmashPig\Core\RetryableException */ public function testRequeueMessage() { $auth = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Authorisation', file_get_contents(__DIR__ . '/../Data/auth.json')); $job = ProcessCaptureRequestJob::factory($auth); $job->execute(); }
/** * Operate the datastore as a queue. Will retrieve messages, one at a time, * from the backing store ensuring that no other running process may obtain * the same message. * * Any message obtained via this function must be either acknowledged (and * thus removed from the backing store) or ignored (whereby it is replaced * into the backing store). Only once one of these operations is completed * may another message be obtained from the backing store. * * If a message has not yet been completely when this function gets called, * it will throw a DataStoreTransactionException exception. * * If there were no messages fitting the filter, null will be returned. * * @param null|string $type The class of message to retrieve (if null retrieves all) * @param null|string $id The correlation ID of the message (if null retrieves all) * @param string[] $customSelectors Array of custom STOMP selectors (e.g 'gateway=adyen') * @param bool $deserialize If true will return an object. Otherwise will return an array * with keys 'headers' and 'body'. * * @throws DataStoreTransactionException * @throws DataSerializationException * @throws DataStoreException * @return KeyedOpaqueStorableObject|array|null */ public function queueGetObject($type = null, $id = null, $customSelectors = array(), $deserialize = true) { $msgObj = $this->queueGetObjectRaw($type, $id, $customSelectors); if ($msgObj && $deserialize) { if (!array_key_exists('php-message-class', $msgObj->headers)) { Logger::warning("Message was serialized without key php-message-class. Cannot re-instantiate.", $msgObj); $this->queueIgnoreObject(); // I'm not a huge fan of this recursion; but I don't know what else to do return $this->queueGetObject($type, $id, $customSelectors); } $className = $msgObj->headers['php-message-class']; if (!class_exists($className)) { Logger::warning("DataStore cannot instantiate object from message. No such class '{$className}'.", $msgObj); throw new DataStoreException("Cannot instantiate class '{$className}'; no such class exists."); } try { $classObj = KeyedOpaqueStorableObject::fromJsonProxy($className, $msgObj->body); $classObj->correlationId = $msgObj->headers['correlation-id']; } catch (DataSerializationException $ex) { Logger::warning("DataStore cannot instantiate object from STOMP message.", $msgObj, $ex); throw $ex; } return $classObj; } elseif ($msgObj && !$deserialize) { return array('headers' => $msgObj->headers, 'body' => $msgObj->body); } else { return null; } }
/** * When two authorizations come in with the same merchant reference, we * should cancel the second one and leave the donor details in pending. */ public function testDuplicateAuthorisation() { $api = $this->config->object('payment-provider/adyen/api', true); $auth1 = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Authorisation', file_get_contents(__DIR__ . '/../Data/auth.json')); $job1 = ProcessCaptureRequestJob::factory($auth1); $job1->execute(); $this->assertEquals(1, count($api->captured), 'Set up failed'); $auth2 = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Authorisation', file_get_contents(__DIR__ . '/../Data/auth.json')); $auth2->pspReference = mt_rand(1000000000, 10000000000); $job2 = ProcessCaptureRequestJob::factory($auth2); $this->assertTrue($job2->execute(), 'Duplicate auths should not clutter damage queue'); $this->assertEquals(1, count($api->captured), 'Captured a duplicate!'); $this->assertEquals($auth2->pspReference, $api->cancelled[0], 'Did not cancel the right authorization'); $this->assertNotNull($this->pendingDatabase->fetchMessageByGatewayOrderId('adyen', $auth1->merchantReference), 'Capture job should leave donor details in database'); }