Synchronizacja w PHP - Meet Magento
-
Upload
wjarka -
Category
Technology
-
view
157 -
download
4
description
Transcript of Synchronizacja w PHP - Meet Magento
Wiktor JarkaCreatuity
Synchronizacja - sekcje krytyczne na przykładzie integracji z systemem lojalnościowym
Definicje
Sekcja krytyczna
“W programowaniu współbieżnym - fragment kodu, w którym korzysta się z zasobu współdzielonego, a co za tym idzie - w danej chwili może być wykorzystywany przez co najwyżej jeden wątek.”
Semafor
“Chroniona zmienna lub abstrakcyjny typ danych, który stanowi klasyczną metodę kontroli dostępu przez wiele procesów do wspólnego zasobu w środowisku programowania równoległego.”
Opis problemu
● Projekt: system zakupów grupowych wyspecjalizowany w polach golfowych,
● Opticard: system punktów lojalnościowych posiadający API,
● typy transakcji: inicjacja karty, dodawanie punktów, zapisanie danych użytkownika,
● sequence number: część specyfikacji Opticard API,● pula numerów kart: część specyfikacji modułu
integrującego Opticard z Magento.
Zachowanie spójności danych
Jak?
Sequence Number
● służy do zachowania spójności Opticard i systemu,
● zwiększany po poprawnej transakcji,● pozostaje taki sam po błędzie,● nie mogą istnieć dwie poprawne transakcje
z tym samym numerem sekwencji.
Pula kart
● administrator Magento definiuje jakie karty mogą być przypisane do klientów,
● dowolna, wolna karta jest przypisywana do użytkownika w momencie, kiedy pierwszy raz coś kupi,
● jedna karta może być przypisana tylko do jednego użytkownika (przez cały “cykl życia” karty).
Podstawowe informacje o systemie
Wersja wyjściowaclass Creatuitycorp_Opticard_Model_Card_Pool extends Varien_Object {
(...)
public function assignAnyFreeCardToCustomer($customerOrId) { $customerId = $this->_helper()->asCustomerId($customerOrId); $freeCards = $this->getFreeCardNumbersCollection() ->addFieldToSelect('card_id') ->addFieldToSelect('card_number'); if ($freeCards->count() == 0) { Mage::throwException('There is no Opticard card numbers available to assign'); } /** @var Creatuitycorp_Opticard_Model_Card $card */ $card = $freeCards->getFirstItem(); $card->assignToCustomer($customerId); $card->save(); return $card; }}
Synchronizacja w PHP
Jest kilka możliwości:● poprzez plik: flock(),● poprzez wbudowany semafor: sem_acquire(),● poprzez mysql: GET_LOCK().
Rozwiązanie: krok 1class Creatuitycorp_Opticard_Model_Card_Pool extends Varien_Object {
public function assignAnyFreeCardToCustomer($customerOrId) { try { $row = $this->_db()->query("SELECT GET_LOCK('" . self::$_lockName . "', 15) AS locked")->fetch(); $isTimeout = $row['locked'] == 0; if ($isTimeout) { $this->_debugLog("Couldn't lock. Timeout exceeded."); Mage::throwException("I was unable to create lock within 15 seconds."); } $this->_db()->beginTransaction();
// LOGIC GOES HERE
$this->_db()->commit(); $this->_db()->query("DO RELEASE_LOCK('" . self::$_lockName . "')"); return $card;
} catch (Exception $e) { $this->_db()->rollback(); $this->_db()->query("DO RELEASE_LOCK('" . self::$_lockName . "')"); throw $e; } }
(...)
$customerId = $this->_helper()->asCustomerId($customerOrId);
$freeCards = $this->getFreeCardNumbersCollection() ->addFieldToSelect('card_id') ->addFieldToSelect('card_number');
if ($freeCards->count() == 0) { Mage::throwException('There is no Opticard card numbers available to assign');}/** @var Creatuitycorp_Opticard_Model_Card $card */$card = $freeCards->getFirstItem();
$card->assignToCustomer($customerId);
$card->save();
Lepiej, ale…?
• Ciężko to pokazać na slajdzie (serio!),• duplikacja kodu dla każdej funkcji, która wymaga
działania w sekcji krytycznej,• kiedy sekcje nachodzą do siebie - RELEASE_LOCK
może być wykonane zbyt wcześnie.
Nachodzące na siebie sekcje
Wymagana transakcyjność!
Rozwiązanie: krok 2.1class Creatuitycorp_Opticard_Model_Mysql_Transactional_Critical_Section extends Creatuitycorp_Opticard_Model_Mysql_Abstract {
public function begin() { $this->_semaphore()->lock(); $this->_transaction()->begin(); }
public function end() { $this->_transaction()->commit(); $this->_semaphore()->release(); }
public function revert() { $this->_transaction()->rollback(); $this->_semaphore()->release(); }
/** @return Creatuitycorp_Opticard_Model_Mysql_Transactions */ protected function _transaction() { return Mage::getSingleton('opticard/mysql_transactions'); }
/** @return Creatuitycorp_Opticard_Model_Mysql_Semaphore */ protected function _semaphore() { return Mage::getSingleton('opticard/mysql_semaphore'); }(...)
Rozwiązanie: krok 2.2class Creatuitycorp_Opticard_Model_Mysql_Semaphore extends Creatuitycorp_Opticard_Model_Mysql_Abstract {
private static $_count = 0; private static $_lockName = 'opticard_semaphore';
public function lock($timeout = 300) { if (self::$_count++ === 0) { $row = $this->_db()->query("SELECT GET_LOCK('" . self::$_lockName . "', " . $timeout . ") AS locked")->fetch(); $isTimeout = $row['locked'] == 0; if ($isTimeout) { Mage::throwException("Unable to lock. Timeout exceeded."); } } }
public function release() { if (self::$_count === 0) { Mage::throwException("Cannot unlock what's not locked. Neither can Chuck Norris."); }
if (--self::$_count === 0) { $this->_db()->query("DO RELEASE_LOCK('" . self::$_lockName . "')"); } }(...)
Rozwiązanie: krok 2.3class Creatuitycorp_Opticard_Model_Card_Pool extends Varien_Object { public function assignAnyFreeCardToCustomer($customerOrId) { try { $this->_criticalSection()->begin();
$customerId = $this->_helper()->asCustomerId($customerOrId); $freeCards = $this->getFreeCardNumbersCollection() ->addFieldToSelect('card_id') ->addFieldToSelect('card_number');
if ($freeCards->count() == 0) { Mage::throwException('There is no Opticard card numbers available to assign'); }
/** @var Creatuitycorp_Opticard_Model_Card $card */ $card = $freeCards->getFirstItem(); $card->assignToCustomer($customerId);
$card->save();
$this->_criticalSection()->end(); return $card; } catch (Exception $e) { $this->_criticalSection()->rollback(); throw $e; } }(...)
Krok 2 - podsumowanie
Minusy
• konieczność dodawania dodatkowego kodu do każdej funkcji używającej sekcji krytycznej
Plusy
• obsługa nachodzących na siebie sekcji krytycznych
• lepsza organizacja i czytelność kodu
Rozwiązanie: krok 3.1class Creatuitycorp_Opticard_Model_Card_Pool extends Varien_Object { public function assignAnyFreeCardToCustomer__CRITICAL_SECTION($customerOrId) { $customerId = $this->_helper()->asCustomerId($customerOrId);
$freeCards = $this->getFreeCardNumbersCollection() ->addFieldToSelect('card_id') ->addFieldToSelect('card_number');
if ($freeCards->count() == 0) { Mage::throwException('There is no Opticard card numbers available to assign'); }
/** @var Creatuitycorp_Opticard_Model_Card $card */ $card = $freeCards->getFirstItem();
$card->assignToCustomer($customerId); $card->save();
return $card; }
public function __call($method, $args) { if ($this->_criticalSection()->shouldRunInCriticalSection($this, $method)) { return $this->_criticalSection()->runMethodInCriticalSection($this, $method, $args); } return parent::__call($method, $args); }(...)
Rozwiązanie: krok 3.2class Creatuitycorp_Opticard_Model_Mysql_Transactional_Critical_Section extends Creatuitycorp_Opticard_Model_Mysql_Abstract {
const METHOD_SUFFIX = '__CRITICAL_SECTION';
public function shouldRunInCriticalSection($object, $methodName) { return method_exists($object, $this->_csMethodName($methodName)); }
public function runMethodInCriticalSection($object, $methodName, $args) { return $this->run($object, $this->_csMethodName($methodName), $args); } protected function _csMethodName($methodName) { return $methodName . self::METHOD_SUFFIX; } public function run($object, $methodName, array $args = array()) { try { $this->begin(); $result = call_user_func_array(array($object, $methodName), $args); $this->end(); return $result; } catch (Exception $e) { $this->revert(); throw $e; } }(...)
Krok 3 - podsumowanie
Minusy
• konieczność nadpisania funkcji __call dla każdej klasy używającej sekcji krytycznej
Plusy
• funkcja robi tylko i wyłącznie to za co jest odpowiedzialna
Pytania?
A jak Ty byś to zrobił?
Dziękuję za uwagę