Propel, Cache и Instance Pooling

Nov 22, 2009 01:19

Ламер - он почему Аццкий? Потому что, если где какие грабли есть - то грабли обязательно будут его.

Вчера столкнулся с интересным глюком.
Дано: Symfony-проект. Ajax.
Есть список элементов, порядок следования которых определяется атрибутом SORT. За работу с этим списком "отвечают" два класса: первый создан на основе таблицы списка - parent_id, child_id, sort.

Второй - базируется на любимом Ламером setSQLinsteadOfView ( http://hellish-lamer.livejournal.com/1298.html ), т.е. "подмене" обращения к таблице результатами работы сырого запроса. Это довольно удобно, т.к. позволяет легко создавать объекты с нужным набором свойств. В данном случае - джойнами вытаскиваются имена родителя и потомка и несколько других нужных для отображения списка атрибутов.

Задача стояла, в целом, элементарная.

По задумке происходило следующее:
- юзер, желая переместить элемент, кликает на стрелку вверх-вниз;
- по клику запускается jq_remote_function, функция дергает экшн;
- экшн получает из реквеста нужные айдишники и дергает статический метод, который меняет порядок списка, перезаписывая атрибуты SORT;
- шаблон экшна - вызов стандартного компонента, заново делающего запрос к базе и отрисовывающего список через sfPropelPager.

Просто? Просто. Хрен. Потому что и тут появились они. Грабли.

Грабли заключались в том, что, когда экшн после изменения порядка списка вызывал компонент, компонент послушно выводил список в правильном порядке - но со старыми значениями свойства SORT элементов списка. Т.е. сортируются элементы по SORT правильно, но при этом они - старые. О как.

При обновлении страницы (или открытии того же урл в новом окне) все становилось на свои места: SORT каждого элемента приобретал ожидаемое значение.

Ламер начал локализовывать проблему:
- после срабатывания метода по изменению порядка списка в базе уже лежали правильные значения;
- если для отображения списка вызвать не модифицированный, а "родной" класс, передав ему айдишники, значение SORT было корректным;
- не имело значения, сколько раз срабатывал селект модифицированного метода: до обновления страницы значения все равно отображались неправильно.

Подозрение пало на какое-то хитрое кеширование. Ламер начал копать.
Первый подозреваемый: doSelect, подмененный модифицированным вариантом:

public static function doSelect(Criteria $criteria, PropelPDO $con = null) {
   self::setSQLinsteadOfView($criteria);
   return parent::doSelect($criteria, $con);
}

Ничего подозрительного. В лог шел запрос, который, если запустить его в сыром виде, выдавал верные результаты.
Дальше: parent::doSelect

public static function doSelect(Criteria $criteria, PropelPDO $con = null) {
   return ModContainerContentPeer::populateObjects(ModContainerContentPeer::doSelectStmt($criteria, $con));
}

Т.е. или дело в doSelectStmt, или в populateObjects.

Проверяем doSelectStmt. Метод возвращает некоторое количество технической информации и строку запроса, который затем выполняется для извлечения данных. Ничего подозрительного.

Смотрим в populateObjects. Тут все интересно.

// populate the object(s)
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
   $key = ModContainerContentPeer::getPrimaryKeyHashFromRow($row, 0);
   if (null !== ($obj = ModContainerContentPeer::getInstanceFromPool($key))) {
      $results[] = $obj;
   } else {
      $obj = new $cls();
      $obj->hydrate($row);
      $results[] = $obj;
      ModContainerContentPeer::addInstanceToPool($obj, $key);
   } // if key exists
}

Проверяем вардампами $row = $stmt->fetch(PDO::FETCH_NUM).
Данные верны. Вывод: в populateObjects идет массив с правильными данными, а объекты возвращаются - уже с неправильными.

Копаем populateObjects дальше. Делаем вардамп каждого из случаев. Бинго:

if (null !== ($obj = ModContainerContentPeer::getInstanceFromPool($key))) {
      $results[] = $obj;
      Common::printVar($row);
      Common::printVar($obj);
}
...

Однако getInstanceFromPool не делает ничего криминального: проверяет, есть ли объекты в Pool, есть есть, достает оттуда.
Вывод: Propel объекты таки кеширует, и таки в некоторых случаях кеширование срабатывает некорректно.
Гуглим.

Первый интересный результат найден тут:
http://www.joelwickard.name/15/symfony-propel-level-object-caching.html

class StoreRegionPeer extends BaseStoreRegionPeer {

public static function doSelectOne(Criteria $criteria, $con = null){
$fc = new sfMemcacheFunctionCache();
$items = $fc->call ('StoreRegionPeer::doSelectOne', $criteria);
return $items;
}

public static function doSelect(Criteria $criteria, $con = null){
$fc = new sfMemcacheFunctionCache();
$items = $fc->call ('StoreRegionPeer::doSelect', $criteria);
return $items;
}
}

Но это не лучший выход: каждый раз в doSelect вмешиваться - не дело.
Ищем по "symfony disable Instance Pooling"
http://www.google.ru/search?hl=ru&source=hp&q=symfony+disable+Instance+Pooling+&btnG=%CF%EE%E8%F1%EA+%E2+Google&lr=&aq=f&oq=
Бинго! Первым же результатом:
http://raw.trac.symfony-project.org/wiki/sfPropel13Plugin

Действительно, есть такая настройка.
instance-pooling: true/false

Но совсем отключать - чревато потерей быстродействия.

В конечном итоге - остановился на варианте:
Propel::disableInstancePooling();

Применяется там, где необходимо. В моем случае - в коде компонента, выстраивающего список.

Вот так как-то.

процесс отладки, грабли, чужие решения, symfony

Previous post Next post
Up