четверг, 7 апреля 2011 г.

Groovy вместе (и вместо) c Java в production. Part 1

Этот пост - некоторое мое размышление о текущем проекте, может кому-то будет просто интересно. А может, у кого-то есть здесь опыт именно в таких вещах, и он прокомментирует. А может, у кого-то просто очень богатый жизненный и профессиональный опыт, и он даст "мудрый совет"  (tm) . Итак.

В последнее время стараюсь сойти с прямых и строгих рельс стальных рельс Java на языки под платформу JVM. Приложение - платформа для систем управления поставками (логистика, розничная торговля и прочее), коотрая включает в себя разные компоненты для интеграции, расчетов прогнозов (статистические и эвристические методы) , планирования заказов, трекинга перевозок и еще много чего. Написана почти вся на Java (с примесью, как водится, SQL, HTML/CSS/Javascript, + в отдельных компонентах встречается флеш и питон). Пока что мы склоняемся к тому, чтобы окончательно выбрать Groovy. Почему же?



Какие были требования к новому дополнительному языку, и в каких местах мы бы хотели его использовать?


1) Тесты. Написание тестов, особенно таких, которые работают в сервлет-контейнере и базой данных, занимает много времени. По разным причинам. 


Первая, это то что сам язык Java весьма громоздкий, и даже отличная поддержка IDE мало помогает. Данные приходится либо загружать в базу из CSV-файлов, либо создавать руками на лету. И то и то требует много дополнительного scaffolding code, писать который неприятно, а на Java, с ее отсутствием поддержки для коллекция, замыканий, Path Expressions и прочих фич так и вовсе утомляет (когда появляется множество тест-классов каждый по 700-1000 строк кода).


Вторая причина довольно специфическая.  Написание контейнерных тестов на Java и их дописывание / отладка часто требует рестартов appserver-a  (у на это JBoss), который занимает 3-4 минуты. Собственно, любое изменение кода, кроме изменения "кода внутри методов" требует рестарта JBoss-a. Поменял название поля класса? Перезапусти JBoss. Поменял тип параметра метода с float на double? Перезапусти JBoss. И так все время.


2) Вокрфлоу и прочая бизнес логика, где скорость выполнения Java-кода (байткода) не является узким местом, а вот писать на Java сложную итерацию по коллекциям, проверку сложных условий, и прочую бизнес логику бывает  утомительно.


3) Поддержка метапрограммирования - в понятном, не слишком зубодробительном виде, но достаточная, чтобы создавать DSL-языки для таких вещей, как моделирование иерархических структур данных в БД, построение экранов с отчетами, конфигурации портлетов - в общем того, что в мире Java-энтерпрайза традиционно программируется в XML-e.


Вообще,  "программирование в XML" - это совершенно отдельная вещь. Это именно то, что Джоэль Спольски называет leaky abstraction., только в чуть расширенном понимании. До тех пор, пока вы пишете много простых, тривиальных и однотипных кусков чего-то (чего-угодно), вы на коне, и ваша продуктивность приятно радует менеджеров. Это при условии, конечно, что автор соответствующего фреймворка (для парсинга этих XML-описания и построения по ним каких-то реальных вещей) правильно предвидел наиболее типичные случаи использования, и сделал для них нормальную поддержку.


Но как только вам надо сделать что-то такое, что автор соответствующего фреймворка не предусмотрел - ждите проблем - появления множества кода с тегами // HACK, // REVISE LATER, // FOLLOWUP, и долгих часов обсуждений, а можно ли это поддержать внутри фреймворка, а сколько на это понадобится времени, чтобы это реализовать, а сколько это будет стоить, а достаточно ли это нужная и важная фича, чтобы ее поддерживать на уровне фреймворка, и прочее, и прочее. А Тикет в джире все это время висит и висит c тегом "Being investigated".
Пока это все с причинами. Теперь, так какой у нас есть выбор?

1) Очевидно, хотелось бы что-то компилирующееся в Java-байткод. Лучше конечно, чтобы была полная двусторонняя интеграция с Java на уровне байткода JVM.
2) Предлагаюших писать на Lisp или Haskell в команде не нашлось.
3) Можно брать языки, портированные под JVM - JRuby, Jython. Jython мы используем кое-где, но только для скриптинга и работы с файлами в основном. Имело бы смысл, если бы кто-то из нас хорошо знал Ruby или Python. Таковые есть, но мало. Не айс.
4) Наконец, можно писать на языках, специально написанных под JVM, с синтаксисом, более похожим на Java, с явным упором на отличную интеграцию с ней, но перенявших фишки языков из предыдущих пунктов. Таких нашлось два - Groovy и Scala (есть еще Clojure, и недавно появившийся Morah, но это пока слишком экзотично).

Scala пока что отпала (к некоторому моему сожалению) по ряду причин - сложновата концептуально, заметный упор в функциональное программирование, выше порог вхождения, круче кривая изучения, меньше (вероятно) ореол использования. Плюсов у нее тоже немало, но в итоге они не перевесили.

Как итог, пока что медленно и осторожно пробуем интегрировать код на Groovy в намеченных областях, и кое-какие выводы уже можно сделать.


Плюсы:
 - Возможность перекомпилировать и перезагружать любые изменения в коде классов под JBoss, в отличие от Java hotswap, который умеет перезагружать "на лету" только изменения кода внутри методов класса. Мелочь, а дух захватывает.
 - Поддержка на уровне языка на списков, мэпов, регулярных выражений, замыканий, GPath-итерации по сложным структурым..метаобъектный протокол, билдеры иерархий, и еще много всего, что есть в Python-e и Ruby  и что делает код короче в разы и приятнее в чтении.


Минусы:
 - Динамическая (опциональная) типизация. Иногда удобно, но ненавижу эти no such property, no method found applicable for the following arguments:... и прочие обратные стороны медали под назвнанием "Rapid Software Development".
 - Редакторы кода и  поддержка IDE. Есть, и неплохая - но намного, НАМНОГО хуже чем для Java в современных IDE. Понятно, что язык динамический и многого не накрутишь..но все же (JetBrains - я в вас верю!!).


Часто упоминаемая на разных форумах и в блогах "низкая скорость выполнения" - не знаю, не заметил. Если вы часто вычисляете ряды фибоначчи (вместо того, чтобы раз посчитать и хранить), вычисляете фракталы Мандельбродта или перемножаете матрицы размером 10000 х 10000 - да, у вас вероятно будет много проблем. Но в типичном Enterprise Java приложении, я так думаю, не менее 60-80% кода можно переписать на Groovy без заметной потери производительности.


Да - мои мысли в этом посте немного связаны с этими.

воскресенье, 3 апреля 2011 г.

Мемоизация рекурсивных замыканий в Groovy - FIXED

Я и забыл написать, чем кончилась вся так история с мемоизацией рекурсивных замыканий - все там оказалось в итоге довольно просто, и бага, который я подозревал, там нет. После 3 минут общения с Jochen-ом (это техлид проекта Groovy), все как-то встало нормально на свои места.

Итак, рассмотрим, как работает мой вариант, тот который я предлагал, и как следует его изменить, чтобы он работал так, как мы ожидаем.

def fact = {it -> sleep(100); it ?  it * call(it - 1g) : 1g}
fact = fact.memoize()
println fact(70)
println fact(71)

Что же происходит в этом случае "за сценой"? Если скомлировать этот код, и декомпилировать получившийся байткод в Java-код, для простоты понимания,  то это будет выглядеть (схематично) примерно так:

class Closure1 extends MemoizedClosure {
   call (param) {
       bla-bla;
       return param * this.call(param-1); // this тут значит Closure1
   }
 } 
}

Т.е. можно видеть, что вызов call() в теле замыкания всегда обращается к тому объекту замыкания, внутри которого (в лексическом контексте которого), он вызывается. Т.е. в нашем случае это всегда Closure1, а не тот враппер, умеющий делать мемоизацию, который возвращает вызов fact.memoize() (да, метод memoize() всегда возвращает новый объъект замыкания).

А как следует правильно записать это мемоизируемо-рекурсивное замыкание? А вот так:

def fact
fact = {it -> sleep(100); it ?  it * fact(it - 1g) : 1g}
fact = fact.memoize()
println fact(70)
println fact(71)

Обратите внимание на выделанное. Мы определяем переменную fact, и присваиваем ей ссылку на создаваемый объект замыкания, после чего создаем враппер для нашего замыкания и присваиваем ссылку на этот враппер переменной fact. И, поскольку внутри тела нашего  замыкания мы не вызываем его метод call() напрямую, а вызываем fact(), то у нас возникает возможность направить рекурсию по правильному пути - т.е. все рекурсивные вызовы теперь будет проходить через наш fact (т.е. через враппер с поддержкой мемоизации). 

В Java-коде это будет (схематично) выглядеть так:

class Closure1 extends MemoizedClosure {
  // эта ссылка будет указывать на fact, т.е. на closure wrapper,
  // который поддерживает мемоизацию.
  private Closure reference 

   call (param) {
       bla-bla;
       return param * reference.call(param-1);
   }
 } 
}
​
Желающие могут запустить исправленный код на http://groovyconsole.appspot.com/ и убедиться в его правильности :).

воскресенье, 13 марта 2011 г.

Groovy - глюк при мемоизации рекурсивных замыканий

Задумал тут написать статью о том, как в Groovy работает мемоизация замыканий.

Для тех кто не знает - это возможность кешировать результаты вычисления (работает, разумеется, только для "чистых" функций) какого-то значения в виде Map-a [набор аргументов функции] -> [значение], ассоциированного с функцией. Позволяет для сложных вычислений с реюзабельными результатми выигрывать во времени выполнения, проигрывая немного (или много, если диапазон результов большой) в памяти.

О ней, в частности, как об одной из интересных фич грядущего Groovy 1.8 писал в своем блоге один раз разработчиков Groovy Рошан Дорани (Roshan Dawrani).

И внезапно, экспериментируя, натолкнулся на следующий, не знаю пока, баг или нет, но нечто странное точно. Итак, пишем рекурсивную лямбда-функцию, считающую факториал, добавляем туда вызова sleep(time), чтобы разница в количестве вызовов была заметна и измерима, и пробуем мемоизировать все это дело. Тестировать online можно тут - http://groovyconsole.appspot.com/script/439003.

Пример 1:


def startTime = System.currentTimeMillis()
def fact = {it -> sleep(100); it ? it * call(it - 1g) : 1g}
println fact(50)
println fact(50)
println ((System.currentTimeMillis() - startTime) / 1000.


Никакой мемоизации здесь нет, и скрипт работает 10.391 секунд.
Теперь попробуем мемоизировать функцию. Для чего вызываем на объекте замыкания (да, я использую термин "замыкание" вместо "лямбда функция", пуристы - простите меня) memoize(), и присваиваем переменной fact ссылку на враппер, который возвращает метод memoise().
Пример 2:

ddef startTime = System.currentTimeMillis()
def fact = {it -> sleep(100); it ? it * call(it - 1g) : 1g}
fact = fact.memoize()
println fact(50)
println fact(50)
println ((System.currentTimeMillis() - startTime) / 1000.0)


​
В этот раз мы мемоизировали наше замыкание, и все работает за 5.175 секунд, т.е. второй раз вызов для того же аргумента отрабатывает почти мгновенно.
Но что если мы хотим вызвать для другого аргумента, и воспользоваться промежуточными результами, которые должны были бы, по идее, быть кешированы ранее?

Пример 3:

def startTime = System.currentTimeMillis()
def fact = {it -> sleep(100); it ? it * call(it - 1g) : 1g}
fact = fact.memoize()
println fact(50)
println fact(51)
println ((System.currentTimeMillis() - startTime) / 1000.0)
​

В приведенном случае я ожидал бы, что скрипт отработает за 5 с небольшим секунд, т.к. он начнет вычислять fact (51) = 51 * fact(50), то что выделено жирным уже должно быть вычислен о строчкой выше. Правда, логично и ожидаемо? Ан-нет, скрипт отработал за 10.519 секунд.

Т.е. мемоизация не работает для этого случая. Почему так происходит? Я смотрю в исходники сейчас, и похоже это потому, что метод memoize() создает враппер для исходного замыкания, и возвращает ссылку на него, а вызов call() изнутри самого замыкания не роутит вызов через этот враппер.

Написал вопрос в девелоперскую рассылку, жду.. Если подтвердят, что баг - может патч написать?

четверг, 10 марта 2011 г.

T-SQL: когда индекс на table variable очень нужен

В мире T-SQL живут и сотрудничают две очень похожие и в тоже время очень разные вещи: табличные переменные (table variables) и временные таблицы (temporary tables). На форумах часто можно встретить темы вида «Что мне лучше использовать и в чем разница?». Но про это я сейчас не расскажу ;)

Тема поста про то как ускорить сканирование табличной переменной, а точнее совсем от него избавиться. Предположим что у вас есть временная таблица (да, начнем с неё). Что делать если она большая и во время выборки по условию происходит сканирование таблицы, которое занимает приличное время. Конечно вы создадите необходимый индекс, сравните время затрачиваемое на создание индекса и на сканы, и решите оставить индекс и быстро сканировать или же убрать индекс и быстро заполнять таблицу.

А что делать если такое необходимо сделать с табличной переменной? Все кто пытался хоть раз это сделать знают, что на табличной переменной можно создать тока primary key и нельзя создать индекс. Но если очень хочется, то индекс создать можно :)

Если вы попытаетесь создать индекс напрямую, движок БД недвусмысленно ответит вам, что нельзя на табличных переменных создавать индексы. Давайте подумает что еще у нас ведет себя как индекс, выглядит как индекс, но не индекс? Конечно же это unique constraint. В MS SQL этот тип constraint реализуется как индекс (да и в большинстве других РСУБД тоже). Так что же нам мешает создать unique constraint на таб
личной переменной и наслаждаться жизнью? Абсолютно ничего не мешает:

 







Поле Id включено в unique constraint потому что комбинация полей может повторяться. Уникальность должна быть уникальной.
Смотрим план запроса:






Index Seek — что можно еще желать?

понедельник, 7 марта 2011 г.

Oracle insufficient privileges внутри хранимой процедуры

Немного об одной тонкости в системе привилегий Oracle, о которой не все знают, и которая может съесть немало времени на отладку.

Суть в следующем -- 
Для хранимых процедур, триггеров и прочего PL/SQL (но НЕ для анонимных блоков PL/SQL!) привилегии, необходимые для выполения команд динамического SQL (например, execute immediate ('create synonym s1 for table_1')) должны быть даны создателю (тому пользователю, в схеме которого выполняется процедура или триггер) явно, а не через роль. Очень важный момент, речь идет не о привилегии на выполнение процедуры, а о привилегиях, необходимых для выполнения команд динамического SQL. 

Подробности тут - www.sql.ru/faq/faq_topic.aspx

Советую разобраться со всеми примерами, это та вещь, которую сложновато бывает отладить без знания того, что происходит внутри.

суббота, 5 марта 2011 г.

sun.misc.Unsafe - немного магии вокруг JVM

Когда-то, выбирая декомпилятор, натолкнулся я на несколько изумительных статей на wasm.ru. Статьи они кажется потерли, или по крайней мере изменили адреса, но сохранили их в виде вордовского файла, который можно загрузить отсюда:



Описывают, что такое Unsafe api, предоставляющее программисту возможность работать с классами, методами и т.п. ниже того уровня , который позволяют штатные средства платформы Java - на фактически на уровне тех самых С-структур, которые используются внутри JVM для предоставления сущностей класс, инстанс класса, метод и т.п.

Что дает возможность делать многие вещи, которые в программировании на Java  "традиционно" считаются невыполнимыми - функция sizeOf, (возвращающая точный размер объекта в памяти , взятый напрямую из поля  структуры, которая представляет этот объект внутри JVM), наследования от final-класса (делается двумя шагами по сути -  в список предков класса добавляется нужный нам класс, и из таблицы модификаторов доступа для этого суперкласса во время выполнения программы удаляется final - и

всего делов ;)).



Ну и уже более изощренные и хакерские штучки - самомодицирующиеся во время выполнения методы..и т.п. Очень рекомендую эти статьи к прочтению :).

Да, конечно -- смещения полей в структурах высчитываются на пальцах, класс sun.mics.Unsafe недокументирован (но присутствует в Java с самого ее начала, и очень навряд ли будет из нее удален - я очень сильно подозреваю, что предоставляемые им низкоуровневые возможности используются инструментами типа отладчиков и т.п.), и в продакшне ни один Project Manager такое использоватьне позволит -- да и не требуется в обычных приложения никогда столь низкоуровневый доступ к JVM. Но знать, как ява-машины работает с классами внутри себя -- полезно и интересно, а знать что из обычной программы на чистой яве можно получать доступ такого уровня -- еще интересней.

И напоследок два слова по поводу того, что это gap в ява-машине, который будет закрыт в следующем же релизе и т.п.

Я не думаю, что это дыра в системе безопасности Java. Собственно, суммирую и повторю описанное в первой из статей.
Итак. Есть две точки входа для получения инстанса класса Unsafe, который позволит нам творить черную магию:

1) Получить его можно через Unsage.getUnsafe() - НО! только в том случае, если вызывающий класс был загружен первичным класслоадером (www.tedneward.com/files/Papers/BootClasspath /BootClasspath.pdf - тут можно прочитать подробней про иерархию класслоадеров). Это сделать несложно -- всего-то добавить ключ -Xbootclasspath в список стартовых опций ява-машины. Но для этого надо иметь доступ  к среде выполнения.

2) Можно просто взять private переменную theUnsafe - которая хранит инстанс класса Unsafe внутри него. Но если есть Security Manager и установлена для него соответствующая политика запрещения опасной рефлексии , получить значение этой закрытой переменной не удастся.

Соответственно, мой вывод -- это API не является уязвимостью в JVM, вовсе нет.
Потому что точно так же рефлексией (если она не запрещена в политиках безопасности) можно творить безобразия внутри приложения, но и польза от нее может быть большая-- надо просто разумно выставлять политики безопасности в каждом конкретном случае.

Да - Unsafe API дает беспрецедентный уровень доступа к среде выполнения Java из программы. Но чтобы получить доступ к этому апи -- надо либо иметь соотв. права на той машине, где выполняется приложение (например, возможность задавать параметры запуска ява-машины), либо должен быть соответственно сконфигурирован (без учета этой опасности) Security Manager.

понедельник, 7 февраля 2011 г.

5 способов реализации thread-safe синглтона на Java


В интернете полно различных вариантов реализации паттерна Singleton на Java. Так же довольно часто задачки на эту тему (реализовать паттерн самому или найти ошибки в представленной реализации) даются на разного рода собеседованиях.

Хорошую статью по реализации синглтонов на Java можно найти например,  здесь.

С точки зрения реализации, есть только одна сложность - синхронизация. И в этом посте я хочу описать 5 способов написать синглтон, который будет корректно работать в многопоточной среде.

Наиболее частой проблемой при реализации многопоточного синглтона является попытка использовать т.н.  double-checked locking (подробно см. JSR 133, например тут). 

В чем же, в двух словах, проблема с этой двойной проверкой? Чисто технически, проблема тут в переупорядочивании инструкций (out-of-order writes), которую вправе делать javac или runtime JVM (JIT-компилятор).

(Еще точнее это разъяснено в комментариях к 133 JSR - "The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object")

Ну а теперь, как можно это пофиксить и написать "правильный" синглтон, работающий в многопоточной среде.

1) Можно просто сделать getInstance() synchronized методом, как и рекомендуют очень многие авторы, и пусть потом профайлер попробует доказать, что именно этот метод является узким местом. В большинстве случаев эта реализация является самой простой и надежной.

2) Можно сделать поле instance volatile-переменной. 
Это будет работать в Java 1.5 и более новых версиях, так как именно в Java 1.5 была сделана ревизия Java Memory Model,  в которой понятие volatile field было более, скажем так, детерминировано - запись volatile поля работает (с точки зрения работы с памятью потока) так же, как и при синхронизации работает monitor release, т.е. сбрасывает кеш потока (ядра, процессора) в shared memory, а чтение volatile поля - как monitor acquire, т.е. инвалидейтит  кеш и заставляет поток перечитать данные из shared memory.

3) Не использовать отложенную инициализацию вовсе, а писать так: 
private static final Singleton instance = new Singleton(),  и никаких проблем не будет (ну кроме той, когда создание синглтона - дорогая операция, а он может и не понадобится в дальнейшем. С другой стороны, в реальных приложениях, как часто вы определяете синглтоны, которые нигде и никогда потом не используются? :)).

4) Четвертый, менее известный прием, заключается в том, чтобы использовать так называемый OnDemand Holder, который нередко называется по имени одного из создателей Java Memory Model - Bill Pugh's singleton. Он основан на механизме инициализации статических полей в Java. Эта самая инициализация происходит в момент загрузки класс в JVM, т.е. в нашем случае это будет при первом вызове метода getInstance (т.к. класс LazySomethingHolder внутренний и более нигде не используется). JVM в таком случае гарантирует, что статическое поле будет корректно инициализирована, и только после этого будет видимо для всех потоков. Такой трюк работает для всех версий Java, в отличие от volatile переменной.

public class Singleton{
    private Singleton(){}

private static class LazySomethingHolder {
  public static Singleton singletonInstance = new Singleton();
}

public static Something getInstance() {
  return LazySomethingHolder.singletonInstance ;
}

5) И, наконец, 5-й способ описал Джошуа Блох во втором издании своей книги "Effective Java". (В первый раз я забыл упомянуть - спасибо что подсказали в комментариях!).  На практике он распространен сравнительно мало, и знают его не все.
Этот способ работает только в Java 1.5+ и основан на том, что (собственно, с Java 1.5)  в Java появились Enum-ы. Поскольку енумы в Java являются тоже объектами (экземплярами класса  java.lang.Enum), они могут содержать любые данные и методы, а элемент енума по сути и является автоматически, singleton instance-ом. Этот подход имеют дополнительные преимущества в том плане безопасности при использовании Serializable и Reflection API. Вот тут есть хорошая статья (на английском) про реализацию синглтонов через енумы.



public enum Elvis {
  INSTANCE; // один элемент енума служит экземпляром синглтона

  // а дальше идут любые методы и данные
  public void leaveTheBuilding() {
    ...
  }
}

P.S. И помните, что паттерн Singleton имеет свои минусы, ибо глобален, зараза. И в ряде случаев  вместо него лучше использовать что-то более гибкое и настраиваемое, например, фабрики.