вторник, 28 июля 2009 г.

XSLT & Google App Engine/Java

Поставил целью запустить простейший XSLT-фреймворк на GAE/J. Требования к процессору:

  1. XSLT 1.0 в полном объеме. А неплохо бы и 2.0 на всякий случай;
  2. Скорость;
  3. Поддержка EXSLT. Хотя-бы частичная, но node-set обязательно.
  4. Расширяемость внешними Java-функциями. Возможность вызова методов класса;
  5. Лицензия на коммерческое применение;
  6. Желательна бесплатность и исходные тексты;
  7. Желательна поддержка TrAX - transformation API for XML.

Вначале долго искал хоть какие результаты тестов производительности. Все найденные данные либо безбожно устарели, либо составлены так, что понять что-либо абсолютно невозможно. Любимый прием авторов сравнений – утаить подробности. Например, пишут “Saxon X.X.X.X”. Что за Saxon? Saxon-B или Saxon-SA? Для дотнета или для явы? И так везде. В общем путем долгих мучений выяснилось что известными более-менее быстрыми процессорами, написанными на яве, являются Gregor, Xalan/XSLTC, XT, Saxon 6.5/Saxon-B/Saxon-SA.

Gregor декларировался как фантастически быстрый компилятор нового поколения. На деле оказался заброшенным коммерческим проектом, абсолютно недоступным для отдельного скачивания. Жаль.

Xalan/XSLTC. Входит в JDK. Правда какой-то древней версии. Попробовал использовать свежий xalan-j 2.7.1. Сразу выяснилось, что по сути XSLTC не поддерживает внешние нестатические функции, да и без их использования валится на GAE с криками “Illegal type in constant pool”. Без использования режима компиляции xalan на GAE работоспособен, но загаживает лог криками “Failed calling setMethod method” и “Failed calling setIndent method”. Т.е. не понимает тег <xsl:output>. Да и самый медленный он из всех перечисленных. Короче – в сад.

XT-20051206. По отзывам – очень быстр. Похоже, что проект заброшен. Не смог обработать мой шаблон даже локально, до GAE не дошел. В брак.

Saxon-SA это коммерческий продукт. Значит и пробовать нужно когда проект работает под нагрузкой и приносит прибыль. Не проверял. Но похоже его и не нужно как-то особо проверять ибо полностью внешне совместим с Saxon-B.

Saxon 6.5.5 сейчас подзаброшен. Не проверял, т.к. каких-либо преимуществ перед Saxon-B не имеет, скорее наоборот.

Saxon-B 9.1.0.7 j. Завелся с полпинка как локально, так и на GAE. Оказался быстрее xalan’а на моем простом примере. Работает стабильно. На нем и остановился.

Единственный замеченный глюк – в некоторых случаях xml-файлы могут самопроизвольно искаться в каталоге “/base/” (и не находиться, естественно). Чтобы этого не происходило, нужно использовать классы FileInputStream и File. В них с обработкой путей всё в порядке.

В итоге получился краткий и лаконичный код. Ключевые моменты выделены жирным.

public class XstlServlet
  extends HttpServlet
{
  @Override
  public void doGet(final HttpServletRequest req,
    final HttpServletResponse resp)
    throws IOException, ServletException
  {
    request = req;
    resp.setContentType("text/html; charset=UTF-8");

    try { 
      getTransformer("default").transform(
        new StreamSource(new FileInputStream("default.xml")),
        new StreamResult(resp.getOutputStream()));
    } catch (TransformerException e) {
      throw new ServletException(e);
    }
  }

  public Transformer getTransformer(final String xsltName)
    throws FileNotFoundException, TransformerException,
      MalformedURLException
 
{
    if (!transformers.containsKey(xsltName)) {
     
final Transformer newXslt = xsltFactory.newTransformer(
       
new StreamSource(new FileInputStream(xsltName + ".xslt")));
      newXslt.setParameter("this", this);
     
transformers.put(xsltName, newXslt);
   
}

    return transformers.get(xsltName);
  }

  //
  // callback methods for xslt transformation
  //

  public static String getServletPath(final Object context)
 
{
   
return ((XstlServlet) context).request.getServletPath();
  }

 
public static String getParameter(final Object context,
    final String paramName)

 
{
   
return ((XstlServlet) context).request.getParameter(paramName);
 
}

 
//
 
// private declarations
 
//

 
private HttpServletRequest request;
 
private final Map<String, Transformer> transformers =
   
new Hashtable<String, Transformer>();
 
private final static TransformerFactory xsltFactory =
    TransformerFactory.newInstance();
}

В xslt-шаблоне для вызова внешних методов применяется классический прием – объявление специального пространства имен, ссылка на наш объект через параметр в корневом узле и передача ссылки на объект как первый параметр вызываемой функции.

<xsl:stylesheet version="1.0"
xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” 
xmlns:this=”http://xml.apache.org/xalan/java/com.test.XstlServlet”
exclude-result-prefixes="this">

 
<xsl:param name="this" />

  <xsl:template match="/">
    <xsl:value-of select="this:getParameter($this, @параметр)" />
    <xsl:value-of select="this:getServletPath($this)" />

  </xsl:template>

</xsl:stylesheet>

Осталось еще кеширование нормальное и многопоточность добавить - и фундамент готов.

UPDATE: Статья обновлена. В настоящее время GAE не имеет своего xslt-процессора. Это моя случайная ошибка – в каталоге lib был забыт saxon9.jar, чего оказалось достаточно чтобы гугл его подхватил.

суббота, 25 июля 2009 г.

Free Private SVN

Если кто-то ищет бесплатное хранилище для своего кода, то большой выбор по разным параметрам доступен на сайте с весьма неинтуитивным названием http://www.svnhostingcomparison.com/

Из бесплатных хостингов для для закрытого ПО там указан только один - http://www.myversioncontrol.com/, от себя могу добавить что использую http://sliksvn.com/, но у My Version Control предложение получше. Нужно будет попробовать.

вторник, 7 июля 2009 г.

MSSQL – fast data upload

Хочу поделиться одним удобным рецептом по загрузке большого количества строк в БД MSSQL.

Вообще для этой задачи обычно используют bcp, bulk insert, IRowsetFastLoad или XML Bulk Load. Все эти способы неплохи и задачу выполняют. Но столько ограничений и столько дополнительный телодвижений нужно сделать, что весь энтузиазм пропадает. Я уже несколько лет как придумал (явно не первый) и использую простой как валенок, достаточно элегантный способ, обеспечивающий сравнимую производительность с вышеперечисленными методами.

Суть способа — передача XML с клиента в хранимую процедуру и последующая обработка упрощенным способом.

Пример процедуры:

create procedure [dbo].[DataInsert]
  @xmlData xml
as
begin
  set nocount on
  declare @result int

  insert into [TempData]
    ([date], [seriesid], [value])
  select
    -- cast добавить по вкусу
    node.value(
'@dateTime', 'varchar(100)') [date],
    node.value(
'@seriesId', 'varchar(10)') [seriesid],
    node.value(
'@value', 'varchar(100)') [value]
  from
    @xmlData.nodes('/root/value') Temp(node)

  set @result = @@error
  if @result <> 0 return @result

  // post processing…
end

Исходные данные – это XML-документ с большим количеством  узлов вида <value seriesId="200007" dateTime="2009-06-01T00:00:00.0" value="1.0" />. Корневой элемент не обязателен, хотя в примере и присутствует.

Ключевой момент процедуры - работа с XML напрямую безо всякого дополнительного мусора типа OPENXML, sp_xml_preparedocument/sp_xml_removedocument.

Просто, удобно, быстро.


UPDATE: должен предупредить, что производительность этого решения для SQL 2008 ENT и SQL 2008 EXPRESS различается как минимум на три (!) порядка на одной и той же машине. По непонятным причинам экспресс конкретно тормозит. Такое впечатление, что он XPATH-парсер для каждого выражения заново загружает.

пятница, 3 июля 2009 г.

Google App Engine Datastore

Это пост-памятка — попытка собрать в одном месте факты о хранилище данных, которое предоставляет Google для разработчиков GAE.

Они, конечно везде кричат что это, мол, bigtable, но конкретной технической информации на самом деле кот наплакал, в основе всего лежит две презентации.

Итак, что мы знаем о лисе:

  1. Разработчик GAE получает кусок дискового пространства для хранения данных в Datastore, которое называется the entities table.
  2. Эта, гм, “таблица” условно делится на две “колонки”: ключ (entity key) и сущность (serialized entity). Т.е. это и близко не реляционное хранилище, это скорее persistence storage.
  3. Формат данных внутри этой таблицы называется protocol buffers. Он же используется в низкоуровневом API.
  4. Ключ имеет определенную структуру. Полный ключ каждой сущности имеет название (kind), значение и может ссылаться на родительскую сущность (parent/ancestor entity). Так что если мы храним иерархию из объектов, то ключи могут выглядеть так:
    /Client:Bob
    /Client:Bob/Contract:1
    /Client:Bob/Contract:2
    /Client:Alice/
    /Client:Alice/Contract:1
  5. Данные в the entities table не лежат просто так. Их физическая укладка отсортирована по ключу: сущность без родителя (root entity), её дочерний объект (child), другая сущность без родителя и т.д. Это дает возможность сгруппировать обрабатываемые данные рядом и сильно экономить время чтения.
  6. Всё что относится к одной корневой сущности называется entity group. В нашем примере их две: Bob и Alice.
  7. Запись с хранилищем осуществляется только в транзакции (txn), зато можно читать без транзакций. И что ещё интереснее, обычные транзакции (local txns)осуществляются именно в пределах entity group, а если нужно обновить сущности в нескольких entity group, то это уже издевательски именуется гугловцами как distributed transaction. О, честь им и хвала!
  8. Транзакции двухфазные. После первой фазы обновлены все сущности, а после второй обновлены все индексы. Причем блокировок по сути нет - после первой фазы новые данные становятся доступны для чтения. Что дает разработчику уникальную возможность запросить одни данные, а прочитать совсем другие!
  9. В случае физических сбоев самого хранилища транзакции накатываются (rolled forvard)! Очень странное решение. Очень. Остается надеяться что этого на практике не бывает.
  10. Локальная транзакция вообще работает на журнальном принципе: каждая сущность хранит дату своей записи, а root entity хранит дату последнего обновления всей entity group. Обновление сущности просто делает копию этой сущности с измененными параметрами и новым временем записи, а первая фаза транзакции обновляет время в root entity. При перестроении индекса (вторая фаза) просто игнорируются сущности со старым временем.

По хранилищу автоматически и вручную строятся индексы:

  1. Само собой есть индекс по полному ключу. И везде подчеркивается что такой доступ самый быстрый.
  2. Есть индекс по kind. Именно на него опирается эмуляция запросов вида select * from Client.
  3. Есть индексы по одному свойству сущности (single-property index). Такие индексы исходно двунаправлены и могут обрабатывать операции сравнения: больше, меньше и равно. На этих же индексах делается сортировка order by. Такие индексы автоматически строятся для простых типов данных строк и чисел.
  4. А вот составные индексы (composite index) нужно определять самостоятельно. Такие индексы могут включать несколько свойств и ссылку на предка (ancestor).
  5. Движок умеет делать слияние (merge join). Т.е. может использовать несколько single-property index и пересечь результаты отбора.

Проблемы хранилища:

  1. Бывают таймауты. Причины непонятны, отрапортовано давно, толку ноль. Т.е. приложение должно уметь обработать таймаут в любой момент.
  2. Иногда бывает техническое обслуживание: доступ только для чтения, отключение доступа. Т.е. приложение должно уметь обработать ограничения доступа в любой момент.
  3. И самое главное — непонятно как это обрабатывать на Яве, т.к. нужные исключения существуют только для питона. Т.е. API для java в принципе не содержит средств для обработки вышеописанных ситуаций. Update: в последних SDK нужные исключения появились.
  4. Update: Блокировки. При записи даже одного объекта блокируется запись во весь entity group. Так что entity groups должны быть маленькие. И желательно не иметь одних и тех же постоянно обновляемых мест. Именно поэтому разработчики упирают на технику называемую sharded counters.

Разные замечания:

  1. Update: О скорости: на практике Native Datastore API на минимум порядок быстрее JDO/JPA. 46ms на одну запись одного объекта в хранилище.
  2. Долгая инициализация хранилища: у меня первый запрос занимает 2 секунды.
  3. Есть ряд приколов в работе с индексами. Например попадание в диапазон.