public function add($item_id, $quantity) { $batches = array(); $local_txn = $this->claimStart(); $table = $this->table(); $sql_add = "INSERT OR IGNORE INTO `{$table}` " . "(`user_id`, `item_id`, `serial`, `properties`, `soft_delete`) " . "VALUES (%i, %i, %i, %s, 0)"; $sql_update = "UPDATE `{$table}` SET `properties` = %s, `soft_delete` = 0 " . "WHERE `user_id` = %i AND `item_id` = %i AND `serial` = %i"; foreach ($quantity->all() as $serial => $properties) { $properties = json_encode($properties); $rs = $this->execute($sql_add, $this->user_id, $item_id, $serial, $properties); if ($rs->affected() < 1) { $rs = $this->execute($sql_update, $properties, $this->user_id, $item_id, $serial); if ($rs->affected() < 1) { if ($local_txn) { Transaction::rollback(); } throw new Exception('database error', $this->dbInfo()); } } } if ($local_txn) { if (!Transaction::commit()) { throw new Exception('database error', $this->dbInfo()); } } return TRUE; }
public function subtract($item_id, $quantity) { $local_txn = $this->claimStart(); $table = $this->table(); $sql = "UPDATE `{$table}` SET `quantity` = `quantity` - %i " . "WHERE user_id = %i AND `item_id` = %i AND `quantity` >= %i"; $rs = $this->execute($sql, $quantity, $this->user_id, $item_id, $quantity); if ($rs->affected() < 1) { if ($local_txn) { Transaction::rollback(); } throw new Exception('not enough', $this->dbInfo()); } $sql = "SELECT `quantity` FROM `{$table}` WHERE `user_id` = %i AND `item_id` = %i"; $rs = $this->execute($sql, $this->user_id, $item_id); $row = $rs->fetch(); $rs->free(); if (!$row) { if ($local_txn) { Transaction::rollback(); } throw new Exception('database error', $this->dbInfo()); } if ($local_txn) { if (!Transaction::commit()) { throw new Exception('database error', $this->dbInfo()); } } return $row['quantity']; }
public function spawn($parent = NULL) { $parent_path = $parent !== NULL ? $this->pathById($parent) : ''; $db = $this->db(); $table = $this->table(); Transaction::start(); $rs = $db->execute("INSERT INTO `{$table}` VALUES ()"); $id = $rs->insertId(); $db->execute("UPDATE `{$table}` SET `path` = %s WHERE `id` = %i", $parent_path . Util::SEP . $id, $id); Transaction::commit(); return $id; }
/** * @see Stockpile_Interface::set(); */ public function set($item_id, $quantity, array $data = NULL) { Transaction::start(); try { $current = $this->get($item_id); if (Base::quantify($current) > 0) { $this->subtract($item_id, $current, $data); } if (Base::quantify($quantity) == 0) { $result = $quantity; } else { $result = $this->add($item_id, $quantity, $data); } if (!Transaction::commit()) { throw new \Gaia\Exception('database error'); } return $result; } catch (\Exception $e) { $this->handle($e); throw $e; } }
public function sort($pos, array $item_ids, $ignore_dupes = FALSE) { $batch = array(); $ct = 0; $local_txn = $this->claimStart(); $table = $this->table(); $sql_insert = "INSERT OR IGNORE INTO `{$table}` (`user_id`, `item_id`, `pos`) VALUES (%i, %i, %i)"; $sql_update = "UPDATE `{$table}` SET `pos` = %i WHERE `user_id` = %i AND `item_id` = %i"; foreach ($item_ids as $item_id) { $pos = bcadd($pos, 1); $rs = $this->execute($sql_insert, $this->user_id, $item_id, $pos); $ct += $curr = $rs->affected(); if (!$ignore_dupes && !$curr) { $rs = $this->execute($sql_update, $pos, $this->user_id, $item_id); $ct += $rs->affected(); } } if ($local_txn) { if (!Transaction::commit()) { throw new Exception('database error', $this->dbInfo()); } } return $ct; }
/** * add a new entry to the skein. returns the id. */ public function add($data, $shard = NULL) { $shard = strval($shard); if (!ctype_digit($shard)) { $shard = Util::currentShard(); } $table = $this->table('index'); $dbi = $this->db($table); DB\Transaction::start(); $dbi->start(); $sql = "INSERT INTO {$table} (thread,shard,sequence) VALUES (%i, %i, @SKEIN_SEQUENCE:=1) ON DUPLICATE KEY UPDATE `sequence` = @SKEIN_SEQUENCE:=( `sequence` + 1 )"; $dbi->execute($sql, $this->thread, $shard); $rs = $dbi->execute('SELECT @SKEIN_SEQUENCE as sequence'); $sequence = NULL; if ($row = $rs->fetch()) { $sequence = $row['sequence']; } $rs->free(); $table = $this->table($shard); $dbs = $this->db($table); $dbs->start(); $sql = "INSERT INTO {$table} (thread, sequence, data) VALUES (%i, %i, %s) ON DUPLICATE KEY UPDATE `data` = VALUES(`data`)"; $dbs->execute($sql, $this->thread, $sequence, $this->serialize($data)); $dbi->commit(); $dbs->commit(); DB\Transaction::commit(); $id = Util::composeId($shard, $sequence); return $id; }
namespace Gaia\Stockpile; use Gaia\DB\Transaction; use Gaia\Test\Tap; $user_id = uniqueUserID(); $item_id = uniqueNumber(1, 1000000); // add several items to the account. $items = array($item_id, uniqueNumber(1, 1000000), uniqueNumber(1, 1000000)); sort($items, SORT_NUMERIC); Transaction::claimStart(); $stockpile = stockpile($app, $user_id); foreach ($items as $id) { $stockpile->add($id); } Tap::ok(Transaction::commit(), 'add many items in a transaction'); // grab all the data at once $all = $stockpile->all(); Tap::is(array_keys($all), $items, 'got back all the items we put in'); // test multi-get $some = $stockpile->get($some_keys = array($items[0], $items[1])); Tap::is(array_keys($some), $some_keys, 'multi-get only grabs the items we specified'); // test multi-get with an item that isn't in the list. $stockpile = stockpile($app, $user_id); do { $not_in_list = uniqueNumber(1, 100000); } while (in_array($not_in_list, $items)); $res = $stockpile->get($not_in_list); Tap::is(quantify($res), 0, 'get for an item that doesnt exist in user inventory returns zero'); $res = $stockpile->get(array($not_in_list)); Tap::is($res, array(), 'multi-get for an item that doesnt exist in user inventory returns empty array');
public function trans_complete($auth = NULL) { if ($auth != Transaction::SIGNATURE) { return Transaction::commit(); } if (!$this->txn) { return $this->core->trans_complete(); } if ($this->lock) { return FALSE; } $res = $this->core->trans_complete(); if (!$res) { return $res; } $this->txn = FALSE; return $res; }
public function delete(array $identifiers) { $db = $this->db(); $table = $this->table(); $local_txn = DB\Transaction::claimStart(); $db->execute("DELETE FROM `{$table}` WHERE `identifier` IN ( %s )", $identifiers); if ($local_txn && !DB\Transaction::commit()) { throw new Exception('database error: unable to commit transaction', $db); } }
/** * bid on an item * only works with those listings that set an opening bid (even if that amount is zero). * We use the proxy-bid system here, as used by ebay: * @see http://en.wikipedia.org/wiki/Proxy_bid * the winning bidder pays the price of the second-highest bid plus the step */ public function bid($id, $bid, array $data = NULL) { // normalize the data. $data = new Store\KVP($data); // create an internal transaction if no transaction has been passed in. Transaction::start(); try { // we assume the current user is always the bidder $bidder = $this->user(); // if no bidder was passed into the constructor, blow up. if (!Souk\Util::validatePositiveInteger($bidder)) { throw new Exception('invalid bidder', $bidder); } // get a row lock on the listing. $listing = $this->get($id, TRUE); if (!$listing || !$listing->id) { throw new Exception('not found', $id); } // need the current time to do some comparisons. $ts = Souk\util::now(); // don't go anywhere if the bidding is already closed. if ($listing->closed) { throw new Exception('closed', $listing); } // can't let the seller bid on the listing. if ($listing->seller == $bidder) { throw new Exception('invalid bidder', $listing); } // step is set when it is a biddable item. if it isn't there, don't allow bidding. if ($listing->step < 1) { throw new Exception('buy only', $listing); } // has time expired on this listing? if ($listing->expires <= $ts) { throw new Exception('expired', $listing); } // make sure we bid enough to challenge the current bid level. // if proxy bidding is enabled we still might not win the bid, // but at least we pushed it up a bit. if ($listing->bid + $listing->step > $bid) { throw new Exception('too low', $listing); } // keep a pristine copy of the listing internally so other wrapper classes can compare // afterward and see what changes were made. // The Souk\stockpile adapter especially needs this so it can return escrowed bids // to the previous bidder. $listing->setPriorState($listing); // if proxy bidding is enabled, this gets a little more complicated. // proxy bidding is where you bid the max you are willing to pay, but only pay // one step above the previous bidder's level. // This is how ebay runs its auction site. // this means when you bid, we track your max amount you are willing to spend, but only // bid the minimum. When the next bid comes in, we automatically up your bid for you // until you go over your max amount and someone else takes the lead. // this approach makes the escrow system more efficient as well since it can excrow your // maximum amount all at once, and then refund when you get outbid or refund the difference // if you get it for a lower bid. if ($data->enable_proxy) { // looks like the previous bidder got outbid. // track their maximum amount, and set the bid based on one step above the previous bid. if ($bid >= $listing->proxybid + $listing->step) { $listing->bid = $listing->proxybid + $listing->step; $listing->proxybid = $bid; $listing->bidder = $bidder; $listing->bidcount = $listing->bidcount + 1; // the other bidder is still the winner of the bid. our bid didn't go over their // max bid amount. Bump up their bid amount to what we bid, and increment the // bid count by 2, since we bid on it, and they bid back. } else { $listing->bid = $bid; $listing->bidcount = $listing->bidcount + 2; } // in this case, not a proxy bid system, just a straight up english auction. // don't worry about previous bidder. we know we bid more than the previous bidder, // so pump up the bid to whatever we passed in. } else { $listing->bid = $bid; $listing->bidder = $bidder; $listing->bidcount = $listing->bidcount + 1; } $listing->touch = $ts; $this->storage()->bid($listing); Transaction::commit(); // done. return $listing; // something went wrong ... } catch (Exception $e) { // revert the transaction ... // if it was created internally, remove it. Transaction::rollback(); // toss the exception again. throw $e; } }
/** * commit a transaction. * connected to the Transaction singleton to support multi-database transactions. */ public function commit() { $args = func_get_args(); $auth = isset($args[0]) ? $args[0] : NULL; if ($this->core instanceof Iface) { return $this->core->commit($auth); } if ($auth != Transaction::SIGNATURE) { return Transaction::commit(); } if (!$this->txn) { return FALSE; } if ($this->lock) { return FALSE; } $f = $this->_[__FUNCTION__]; $res = (bool) $f($auth); if (!$res) { return $res; } $this->txn = FALSE; return $res; }
/** * @see Souk::close() * close the bid and transfer currency from escrow into seller, and items to buyer. */ function close($id, array $data = NULL) { // wrap in try catch so we can manage transactions. try { // kick off a transaction if not attached already. Transaction::start(); // do the core logic of closing the listing. $listing = $this->prepListing($this->core->close($id)); // did someone successfully buy this listing? if ($listing->buyer) { // settle up! // start by transferring funds from the buyer's escrow to the seller's currency account. $buyer = $this->transfer($this->currencyEscrow($listing->buyer), $this->currencyAccount($listing->seller)); // subtract moves money from escrow into seller's currency. $buyer->subtract($this->currencyId(), $listing->bid, $this->prepData($data, $listing, 'pay_seller')); // set up a transfer between the buyer's item account and the seller's escrow $buyer = $this->transfer($this->itemAccount($listing->buyer), $this->itemEscrow($listing->seller)); // now, move the item from escrow into the buyer's item account. $buyer->add($listing->item_id, $listing->quantity, $this->prepData($data, $listing, 'winbid')); // the buyer only pays the bid amount, not the max they were willing to pay, // since this is a proxy bid system. // that means if we escrowed extra money, we return it now. if ($listing->proxybid > $listing->bid) { // figure out how much extra was escrowed. $diff = $listing->proxybid - $listing->bid; // set up a transfer between currency escrow and the buyer's currency account. $buyer = $this->transfer($this->currencyAccount($listing->buyer), $this->currencyEscrow($listing->buyer)); // return the funds. $buyer->add($this->currencyId(), $diff); } // no one won the bid? WTF? Return the item to the owner. } else { // set up a transfer between the seller and their escrow account. $seller = $this->transfer($this->itemAccount($listing->seller), $this->itemEscrow($listing->seller)); // return the item from the listing. $seller->add($listing->item_id, $listing->quantity, $this->prepData($data, $listing, 'no_sale')); // if anyone bid on the listing, return their escrowed bid ... this happens if the reserve isn't met. $this->cancelBid($listing, $data); } // commit the transaction if we started one internally. Transaction::commit(); // all done. return $listing; } catch (Exception $e) { // what happened? roll back the transaction Transaction::rollback(); // exception, get your freak on! Fly! be free! throw $e; } }
} if (!isset($buyer_id)) { $buyer_id = uniqueUserId(); } if (!isset($item_id)) { $item_id = uniqueNumber(1, 100000); } Transaction::start(); $souk = souk($app, $seller_id); $listings = array(); for ($i = 1; $i <= 16; $i++) { $listing = $souk->auction(array($i % 2 == 0 ? 'bid' : 'price' => ($i * 10 + 1) % 9, 'item_id' => $item_id)); $listings[$listing->id] = $listing; Time::offset(3600 * 12 + 91); } Transaction::commit(); $ids = souk($app)->search(array('sort' => 'expires_soon', 'item_id' => $item_id, 'seller' => $seller_id)); Tap::cmp_ok(count($ids), '>', 12, 'search expires_soon found results'); $found = TRUE; foreach ($ids as $id) { if (isset($listings[$id])) { continue; } $found = FALSE; break; } Tap::ok($found, 'returned only rows we created'); $owned = TRUE; foreach ($ids as $id) { if ($listings[$id]->seller == $seller_id) { continue;
/** * add a new entry to the skein. returns the id. */ public function add($data, $shard = NULL) { $shard = strval($shard); if (!ctype_digit($shard)) { $shard = Util::currentShard(); } $table = $this->table('index'); $dbi = $this->db($table); DB\Transaction::start(); $dbi->start(); $sql = "INSERT OR IGNORE INTO {$table} (thread,shard,sequence) VALUES (%i, %i, 1)"; $rs = $dbi->execute($sql, $this->thread, $shard); if (!$rs->affected()) { $sql = "UPDATE {$table} SET `sequence` = `sequence` + 1 WHERE `thread` = %i AND `shard` = %i"; $dbi->execute($sql, $this->thread, $shard); } $sql = "SELECT `sequence` FROM {$table} WHERE `thread` = %i AND `shard` = %i"; $rs = $dbi->execute($sql, $this->thread, $shard); $sequence = NULL; if ($row = $rs->fetch()) { $sequence = $row['sequence']; } $rs->free(); $table = $this->table($shard); $dbs = $this->db($table); $dbs->start(); $sql = "INSERT OR IGNORE INTO {$table} (thread, sequence, data) VALUES (%i, %i, %s)"; $data = $this->serialize($data); $dbs->execute($sql, $this->thread, $sequence, $data); if (!$rs->affected()) { $sql = "UPDATE {$table} SET `data` = %s WHERE `thread` = %i AND `sequence` = %i"; $dbs->execute($sql, $data, $this->thread, $sequence); } $dbi->commit(); $dbs->commit(); DB\Transaction::commit(); $id = Util::composeId($shard, $sequence); return $id; }
/** * @see Stockpile_Interface::subtract(); */ public function subtract($item_id, $quantity = 1, array $data = NULL) { Transaction::start(); try { if (!$quantity instanceof Stockpile_HybridQuantity) { $quantity = $this->get($item_id)->grab($quantity); } if ($quantity->value() < 1) { throw $this->handle(new Stockpile_Exception('cannot subtract: invalid quantity', $quantity)); } if ($quantity->tally() > 0) { $tally = $this->core->subtract($item_id, $quantity->tally(), $data); } else { $tally = $this->core->get($item_id, $with_lock = TRUE); } if (count($quantity->all()) > 0) { $serial = $this->serial->subtract($item_id, $quantity->all(), $data); } else { $serial = $this->serial->get($item_id); } if (!Transaction::commit()) { throw new Exception('database error'); } return $this->quantity(array('tally' => $tally, 'serial' => $serial)); } catch (\Exception $e) { if (Transaction::inProgress()) { Transaction::rollback(); } $e = new Exception('cannot subtract: ' . $e->getMessage(), $e->__toString()); throw $e; } }
public function commit($auth = NULL) { if ($auth != Transaction::SIGNATURE) { return Transaction::commit(); } if (!$this->txn) { return parent::commit(); } if ($this->lock) { return FALSE; } return parent::commit(); }
new Transfer($trade, $other); } catch (\Exception $e) { } Tap::ok($e instanceof \Exception && preg_match('/transfer/i', $e->getMessage()), 'no nesting of transfers'); $e = NULL; Transaction::claimStart(); try { new Transfer($core, $core); } catch (\Exception $e) { } Tap::ok($e instanceof \Exception && preg_match('/need\\stwo/i', $e->getMessage()), 'enforce two different parties to trade'); $e = NULL; Transaction::reset(); try { new Transfer(stockpile($app, $other_id), stockpile($app, $other_id)); } catch (\Exception $e) { } Tap::ok($e instanceof \Exception && preg_match('/transaction/i', $e->getMessage()), 'blow up when no transaction'); $e = NULL; try { new Transfer(stockpile($app, $other_id), stockpile($app, $other_id)); } catch (\Exception $e) { } Tap::ok($e instanceof \Exception && preg_match('/transaction/i', $e->getMessage()), 'blow up when neither has a transaction'); Transaction::reset(); Transaction::claimStart(); $trade = new Transfer(stockpile($app, $user_id), stockpile('test2', $user_id)); Tap::is(quantify($trade->subtract($item_id)), 2, 'move items for same user to different app (escrow example with transaction)'); Tap::is(quantify($trade->add($item_id)), 3, 'add it back from other app to main'); Tap::ok(Transaction::commit(), 'transaction commits successfully');
public function commit($auth = NULL) { if ($auth != Transaction::SIGNATURE) { return Transaction::commit(); } if (!$this->txn) { return parent::query('COMMIT'); } if ($this->lock) { return FALSE; } $res = parent::query('COMMIT'); if (!$res) { return $res; } $this->txn = FALSE; return $res; }