barbitoff programmer`s blog

Здесь я публикую заметки из программерской жизни: грабли, на которые мне случилось наступить, проблемы, для которых было найдено элегантное (или не очень) решение, а также все, с чем мне пришлось столкнуться и чем хотелось бы поделиться =)
PS Если хотите меня поблагодарить - на странице есть 3 места, чтобы это сделать =)

пятница, 23 декабря 2016 г.

Birt: определение числа строк в DataSet'е

Задача

Необходимо определить число строк в некотором DataSet'е, чтобы затем использовать это значение в скрипте.

Решение

Как вариант - создаем JS-переменную, равную 0, и в обработчике onFetch нужно датасэта выполняем ее инкремент.

воскресенье, 18 декабря 2016 г.

WSO2 ESB: как отключить динамическую регулировку числа потоков, слушающих JMS-очередь

Есть прокси-сервис, слушающий JMS-очередь. Работа в несколько потоков для него настроена параметрами transport.jms.ConcurrentConsumers и transport.jms.MaxConcurrentConsumers (см. http://axis.apache.org/axis2/java/transports/jms.html). Однако, каким бы ни было значение transport.jms.ConcurrentConsumers, в "состоянии покоя", т.е. когда в очереди-источнике сообщений нет, на очереди висит только 1 получатель. По мере появления сообщений в очереди число получателей растет вплоть до указанного в transport.jms.MaxConcurrentConsumers ограничения. Затем, после опустошения очереди, получатели начинают отключаться, пока снова не остается только 1 получатель.
Объясняется такое поведение вот чем. Изначально шина создает для очереди столько слушателей, сколько указано в параметре transport.jms.ConcurrentConsumers. Каждый получатель начинает цикл опроса очереди, при этом на каждой итерации он ожидает получения сообщения в течение некоторого промежутка времени, заданного параметром transport.jms.ReceiveTimeout (по-умолчанию 1000мс). После определенного числа "холостых" итераций, в результате которых таймаут вышел, а сообщение так и не было получено, слушатель самозавершается. Число итерацией определяется параметром transport.jms.IdleTaskLimit и по-умолчанию равно 10. В итоге, через примерно 10 секунд завершаются все слушатели кроме одного "дежурного", который не завершается ни при каких обстоятельствах.
Данная ситуация не во всех случаях может устраивать, иногда нужно, чтобы очередь всегда слушало фиксированное число слушателей (например, при использовании группировки сообщеий). Тут варианта 2:
  1. Устанавливаем очень большое значение параметра transport.jms.IdleTaskLimit (т.к. бесконечное значение установить нельзя, можно поставить, к примеру, 2147483647)
  2. Устанавливаем бесконечный таймаут ожидания получателем сообщения путем установки параметра transport.jms.ReceiveTimeout в -1

среда, 30 ноября 2016 г.

Oracle: проверка наличия ограничения на таблицу с помощью SQL-запроса

Задача

Проверить с помощью SQL, есть ли на таблице K ограничение (например, внешний ключ) с именем N.

Решение

SELECT * FROM ALL_CONSTRAINTS WHERE TABLE_NAME = 'K' AND CONSTRAINT_NAME = 'N'

пятница, 25 ноября 2016 г.

Бесследное удаление КриптоПРО JCP 2.x

Под Windows:
  • Сносим JDK
  • Удаляем из реестра HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Prefs\ru\/Crypto/Pro
Под Linux:
  • Сносим JDK
  • Удаляем /root/.java/
  • Удаляем /var/opt/cprocsp/
  • Если /usr/java/default не совпадает с только что снесенной JDK, то нужно удалить из нее .java/.systemPrefs/.ru (т.к. эта папка создается JCP почему-то именно в этой JDK, а не в той, куда ставится JCP)

четверг, 24 ноября 2016 г.

PostgreSQL: игнорирование вставки записей в таблицу средствами СУБД

Задача

Средствами СУБД заблокировать вставку записей, удовлетворяющих некоторому критерию, в определенную таблицу. При этом соотв. INSERT-запросы не должны возвращать ошибок, просто строки не должны вставляться.

Решение

Создаем триггерную функцию, блокирующую вставку записи (блокировка вставки выполняется посредством возврата NULL из триггерной функции):
CREATE OR REPLACE FUNCTION nop()
RETURNS trigger AS
$BODY$
BEGIN
RETURN NULL;
END;
$BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
Теперь вешаем эту функцию как триггер на таблицу, прописывая нужное условие, например, равенство значения определенной колонки определенной строке:
CREATE TRIGGER suppress_tr1code_rows
BEFORE INSERT ON mytable
FOR EACH ROW
WHEN (new.code = 'TR1')
EXECUTE PROCEDURE nop();
Профит! 

TortoiseHG: сохраняем логин/пароль для работы с репозиторием

Задача

Для работы с некоторым репозиторием нужно сохранить логин/пароль, чтобы он не запрашивался при каждом pull'е / push'е.

Решение

В домашней директории создаем файл .hgrc и в нем прописываем:
[auth]
myrepo.prefix = https://myrepo/repo/
myrepo.username = myusername
myrepo.password = MyPa$$W0Rd

пятница, 18 ноября 2016 г.

Maven и наследование версий

Задача

Есть проект со сложной иерархией - корневая pom-ка содержит ряд модулей, каждый из модулей, в свою очередь, так является многомодульным и т.п.:
pom.xml
 |_ module1
     |_ pom.xml
     |_ submodule1
         |_ pom.xml
     |_ submodule2
         |_ pom.xml
 |_ module2
     |_ pom.xml
     |_ submdule3
         |_ pom.xml
Есть желание задавать версию всего проекта только в корневой pom-ке, чтобы все вложенные модули ее наследовали.

Однако, не все так просто, как хотелось бы. Ситуация следующая:
  • Дочерний проект может не указывать в своей pom-ке версию, тогда версия подтянется из родительского проекта. И это хорошо.
  • Однако, когда мы в pom-ке дочернего проекта указываем родителя, его версия является обязательной, хотя, казалось бы, при указании relativePath в этом нет необходимости - определение родительского проекта по указанному относительному пути в любом случае будет однозначным
Решение

Решение нетривиально и подсказано https://www.igorkromin.net/index.php/2015/11/08/getting-around-mavens-parent-child-project-version-dependency-issue/. В качестве версии родительского проекта во всех дочерних pom-ках указываем не конкретную версию, а диапазон версий, заведомо более широкий, чем реально возможные версии проекта:
<parent>
<groupId>somegroup</groupId>
<artifactId>someparent</artifactId>
<version>[1.0,99.0)</version>
<relativePath>../</relativePath>
</parent>
В итоге, версия задается только в верхнеуровневой pom-ке и пропагируется во все дочерние проекты.
Подход работает на Maven 3.2.2+.

среда, 26 октября 2016 г.

Java: парсинг дат сразу по нескольким шаблонам

Задача

Есть некая дата/время в строковом представлении, при этом она может быть в одном из нескольких различных форматов (допустим, с миллисекундами или без, с таймзоной или без нее). Необходимо распарсить такую дату.

Решение

Конечно, можно использовать несколько SimpleDateFormat и цепочку из try-catch, т.е. пробовать распарсить с использованием очередного формата, ловить ParseException, после чего пробовать следующий формат. Но можно воспользоваться библиотекой commons-lang от Apache, ниже приведен пример ее использования на Groovy:
import org.apache.commons.lang.time.DateUtils;
def sourceFormats = [
"yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
"yyyy-MM-dd'T'HH:mm:ssXXX"
];;
sourceFormats = sourceFormats.toArray(new String[1]); // convert ArrayList to String Array
def date = DateUtils.parseDate(stringValue, sourceFormats);

PostgreSQL: генерация дат от определенной даты до текущей

Задача

В PostgreSQL необходимо сгенерировать строки с датами от определенной даты (допустим, это значение MIN() по определенному полю некоторой таблицы) до текущей даты.

Решение

На помощь приходит функция generate_series, которая может генерировать строки как с целочисленными значениями, так и со значениями типа timestamp:
SELECT generate_series(MIN(my_date)::timestamp, CURRENT_TIMESTAMP, '1 day')::date FROM mytable

пятница, 21 октября 2016 г.

Java: Выгрузка в файлы бинарных массивов из хип-дампа

Задача

Выгрузить в файлы все бинарные массивы с объемом больше 10Мб из хип-дампа.

Решение

OQL-скрипт:

var i = 0;
map(
  heap.objects('byte[]', false, 'sizeof(it)>10000000'),
  function(v) {
    var file = new java.io.FileOutputStream("T:/binaries/"+i+".bin");
    try {    
      for(var j=0;j<v.length;j++) {
        file.write(v[j]);
        file.flush();
      }
    } finally {
      file.close();
    }
    i++;
    return v;
  }
);
А вообще, экспортировать массивы байт в файл умеет Netbeans, правда, только по одному массиву за раз. Для этого необходимо выделить нужный массив в списке экземпляров, и нажать "Сохранить в файл" под окошком с заголовком "Элементы массива". В качестве выходного формата поддерживается как csv, так и bin (последний наиболее удобен, если нас интересует непосредственно бинарный контент, а не элементы массива байт по-отдельности). И, кстати, экспорт в файл Natbeans делает на пару порядков быстрее, чем приведенный выше скрипт.

четверг, 20 октября 2016 г.

OQL: запись данных из хип-дампа в файл

Бывают ситуации, когда средств OQL недостаточно для проведения требующегося анализа хип-дампа, и проще выгрузить нужные данные в файл и проанализировать их внешним инструментом. 
К примеру, столкнулся со следующей задачей: есть некий класс со строковым полем. В хип-дампе около 10 миллионов объектов данного класса, и нужно по ним собрать статистику, какое значение данного строкового поля встречается сколько раз. В OQL нет аналога SQL-ного GROUP BY, поэтому, поломав голову над реализацией группировки с применением комбинаций map / sort / filter и упершись в то, что полученное решение работает крайне медленно, я решил выгрузить все строки в файл, и потом уже обработать этот файл каким-нибудь более удобным, чем OQL, инструментом.
Например, следующий код выведет в файл namespaces.log пространства имен родительских элементов всех объектов класса OMTextImpl:
var file = new java.io.FileWriter("T:/namespaces.log");
unique(map(
  heap.objects('org.apache.axiom.om.impl.llom.OMTextImpl'),
  function(v) {
    var ns = v.parent == null ? "null_parent" :(v.parent.ns == null ? "null_ns" : (v.parent.ns.uri == null ? "null_uri" : v.parent.ns.uri));
    file.write(new java.lang.String(ns.toString() + "\r\n"));
    file.flush();
    return "1";
  }
));
А такой код - локальные имена всех объектов OMElementImpl:
var file = new java.io.FileWriter("T:/omelements.log");
unique(map(
  heap.objects('org.apache.axiom.om.impl.llom.OMElementImpl'),
  function(v) {
    file.write(new java.lang.String(v.localName));
    file.write(new java.lang.String("\r\n"));
    file.flush();
    return "1";
  }
)); 
Зачем нужен unique и return "1": дело в том, что, по крайней мере, Netbeans считает, что если OQL вернул 100 объектов, нужно прекратить итерацию, вывести эти 100 объектов пользователю и написать, что, де, "слишком много результатов, уточните запрос".  Т.е. если из запросов выше убрать unique, в файл будут записаны значения только из первых 100 найденных объектов заданного класса. Использование же unique заставит OQL-движок продолжать итерации, пока не будет найдено 100 уникальных значений, а т.к. map-функция всегда возвращает константу "1", это эквивалентно итерации по абсолютно всем объектам.
PS Хорошая справка по OQL: http://visualvm.java.net/oqlhelp.html

вторник, 18 октября 2016 г.

WSO2 ESB 4.9.0: настройка числа слушателей JMS-очереди

Проблема

Есть несколько прокси-сервисов, слушающих различные JMS-очереди (в качестве JMS-сервера используется Apache ActiveMQ). В каждом прокси-сервисе число слушателей настроено через соотв. параметры:
<parameter name="transport.jms.ConcurrentConsumers">25</parameter>
<parameter name="transport.jms.MaxConcurrentConsumers">25</parameter>
Однако, в действительности при большой интенсивности поступления сообщений в очереди шина ведет себя достаточно странно. Суммарное число слушателей всех очередей никогда не превосходит 20, причем эти 20 слушателей иногда распределяются между очередями совершенно неприемлемым образом: одна из очередей остается вообще без слушателей (несмотря на поступление в нее сообщений), а все доступные слушатели переключаются на другую очередь, в которую интенсивно поступают сообщения.

Решение

Максимальное число слушателей для очередей ограничивается не только соотв. параметром, задаваемым на уровне прокси-сервиса. Во-первых, его можно настроить на глобально для всего JMSListener'а на уровне axis2.xml:
<transportReceiver name="jms" class="org.apache.axis2.transport.jms.JMSListener">
 <parameter name="MYJMS" locked="false">
  <parameter name="java.naming.factory.initial" locked="false">
    org.apache.activemq.jndi.ActiveMQInitialContextFactory
  </parameter>
  <parameter name="java.naming.provider.url" locked="false">
    tcp://localhost:61616?jms.prefetchPolicy.queuePrefetch=0
  </parameter>
  <parameter name="transport.jms.ConnectionFactoryJNDIName" locked="false">
    QueueConnectionFactory
  </parameter>
  <parameter name="transport.jms.ConnectionFactoryType" locked="false">
    queue
  </parameter>
  <parameter name="transport.jms.ConcurrentConsumers" locked="false">
    50
  </parameter>
  <parameter name="transport.jms.MaxConcurrentConsumers" locked="false">
    50
  </parameter>

 </parameter>
</transportReceiver>
Задаваемые здесь ограничения имеют приоритет над указываемыми на уровне прокси-сервиса. Но в данном случае в axis2.xml никаких ограничений прописано не было, так что проблема была в другом. JMS-слушатели формируются из ограниченного пула "server workers", размер которого настраивается системными свойствами snd_t_core и snd_t_max. Значение по-умолчанию равно тем самым 20, в которые мы и упираемся. Прописываем в wso2seerver.sh параметр -Dsnd_t_max=<N>, где N нужно выбрать таким образом, чтобы сумма значений transport.jms.MaxConcurrentConsumers, заданных во всех прокси-сервисах, слушающих JMS-очереди, не превышала этого значения.

четверг, 22 сентября 2016 г.

Msxml2.XMLHTTP: работа за проксёй в локальном скрипте

Итак, есть некий локальный JScript-скрипт, выполняемый на виндовой машине через cscript. В нем используется XHR-запрос, объект при этом создается следующим образом:
var xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
При этом соединение с нужным ресурсом выполняется по https. 
Все было в порядке, пока скрипт не был перенесен на другую машину, где при попытке загрузить XML по старому URL-у вдруг стала падать ошибка:
msxml3.dll: Security certificate required to access this resource is invalid
Ладно, предположим, что-то не так с SSL-сертификатом, хотя раньше проблем с ним не было. Переходим на объект Msxml2.ServerXMLHTTP и отключаем валидацию сертификатов:
var xmlhttp = new ActiveXObject("Msxml2.ServerXMLHTTP");
xmlhttp.setOption(2, 13056);
(почему 2 и 13056 - см. https://msdn.microsoft.com/en-us/library/ms763811(v=VS.85).aspx). Проблема с сертификатом пропала, зато теперь получаем:
msxml3.dll: The operation timed out
Идем дальше. меняем объект на Msxml2.ServerXMLHTTP.6.0, поддерживающий установку таймаутов:
var xmlhttp = new ActiveXObject("Msxml2.ServerXMLHTTP.6.0");
xmlhttp.setTimeouts(60000, 60000, 60000, 60000);
xmlhttp.setOption(2, 13056);
Однако, проблема продолжает воспроизводиться, по прошествии минуты падает таймаут, хотя загружаемый ресурс 100% доступен и отвечает за время в пределах 10сек. И тут возникает мысль, что "проблемная" машина сидит за прокси-сервером, пробуем:
var xmlhttp = new ActiveXObject("Msxml2.ServerXMLHTTP.6.0");
xmlhttp.setTimeouts(60000, 60000, 60000, 60000);
xmlhttp.setOption(2, 13056);
xmlhttp.setProxy(2, '<proxyhost>:<proxyport>','');
xmlhttp.open('GET', 'http://myurl', false);
xmlhttp.setProxyCredentials('<proxy_user>', '<proxy_password>');
Вуаля, проблема решена.

четверг, 15 сентября 2016 г.

PostgreSQL: использование переменного интервала в запросах

Задача

Есть некая хранимка, в ней в целочисленной переменной N хранится число часов. Необходимо использовать это число в арифметическом выражении для дат, вычтя из текущей метки времени указанное число часов.

Решение

Казалось бы, все просто:

CURRENT_TIMESTAMP - INTERVAL (N || ' hour')

Но не тут-то было, такое выражение вызывает ошибку. Приходится выкручиваться:

CURRENT_TIMESTAMP - (N::varchar || ' hour')::interval


IE: как побороть режим совместимости

Проблема

У пользователя в IE стоит галка "Display intranet sites in Compability View", из-за которой некий сайт во внутренней сетке отображается коряво. Нужно исправить ситуацию, не беспокоя конечного пользователя.

Решение

Если есть доступ к исходникам сайта, можно воспользоваться мета-тегом X-UA-Compatible, вставив его во все html-страницы:
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
Этот мета-тег превалирует над настройками совместимости браузера. Для того, чтобы тег работал, он должен идти в блоке <head> перед остальными элементами (исключение составляют <title> и другие мета-элементы). 
Другой вариант - воспользоваться аналогичным http-заголовком X-UA-Compatible, эффект будет тот же.

среда, 17 августа 2016 г.

Spring Security и аутентификация по Active Directory: ошибка "Failed to locate directory entry for authenticated user"

Проблема

Настроил в приложении, использующем Spring Security, аутентификацию по Active Directory (провайдер ActiveDirectoryLdapAuthenticationProvider), но при попытке входа ловлю ошибку:
2016-08-17 11:20:45.525 ERROR 22440 --- [http-nio-8080-exec-154] ctiveDirectoryLdapAuthenticationProvider : Failed to locate directory entry for authenticated user: user@domain.local
javax.naming.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-0310020A, problem 2001 (NO_OBJECT), data 0, best match of:
'DC=domain,DC=local'

 ]
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3160) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3081) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2888) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.searchAux(LdapCtx.java:1846) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1769) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1786) ~[na:1.8.0_60]
at com.sun.jndi.toolkit.ctx.ComponentDirContext.p_search(ComponentDirContext.java:418) ~[na:1.8.0_60]
at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search(PartialCompositeDirContext.java:396) ~[na:1.8.0_60]
at javax.naming.directory.InitialDirContext.search(InitialDirContext.java:297) ~[na:1.8.0_60]
at org.springframework.security.ldap.SpringSecurityLdapTemplate.searchForSingleEntryInternal(SpringSecurityLdapTemplate.java:333) ~[spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.searchForUser(ActiveDirectoryLdapAuthenticationProvider.java:310) ~[spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.doAuthentication(ActiveDirectoryLdapAuthenticationProvider.java:144) ~[spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:82) [spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
Причина

Провайдер ActiveDirectoryLdapAuthenticationProvider работает следующим образом: 
  1. сначала он пытается забиндиться к AD с указанным логином / паролем
  2. после этого ищет объект пользователя по совпадению введенного логина и атрибута userPrincipalName
Попробовал выполнить аналогичный поиск по введенному логину в AD через curl (http://barbitoff.blogspot.ru/2016/08/ldap-ad-ldapsearch.html), поиск не дал результатов. Посмотрел на объект пользователя - он вообще не имеет атрибута userPrincipalName (не большой специалист по AD, поэтому не могу сказать, почему). К счастью, фильтр, используемый для поиска пользователя в ActiveDirectoryLdapAuthenticationProvider кастомизируется путем вызова setSearchFilter().

Как подключиться по LDAP к AD, если под рукой нет ldapsearch

Проблема

Есть AD-сервер, по которому есть следующая информация:
  • IP
  • юзер и пароль
Хочется полазить по нему, но под рукой есть только Linux машина без ldapsearch.

Решение

Зато на машине нашелся curl. Выполняем:
curl -u <user>@<domain>:<password> "ldap://<AD_IP>:389/"
и получаем в выдаче уже кое-какую интересную информацию о сервере. Далее можно выполнять поисковые запросы (см. https://technet.microsoft.com/en-us/library/aa996205(v=exchg.65).aspx) и искать все, что нужно. Например, найти пользователя по логину:
curl -u <user>@<domain>:<password> "ldap://<AD_IP>:389/DC=company,DC=org?memberOf,sAMAccountName?sub?(sAMAccountName=someusr)"
Или посмотреть список всех OU первого уровня:
curl -u <user>@<domain>:<password> "ldap://<AD_IP>:389/DC=company,DC=org?distinguishedName?one?(objectClass=organizationalUnit)" 
По формату используемого LDAP URL можно посмотреть в соотв. RFC: http://www.ietf.org/rfc/rfc2255.txt.

четверг, 11 августа 2016 г.

Spring Security и "javax.naming.PartialResultException [Root exception is javax.naming.CommunicationException: xxx.yyy:389 [Root exception is java.net.UnknownHostException : xxx.yyy]]"

Проблема

Неожиданно в приложении, использующем для аутентификации и авторизации Spring Security + Misrosoft Active Directory, перестала работать аутентификация из-за ошибки:
javax.naming.PartialResultException [Root exception is javax.naming.CommunicationException: xxx.yyy:389 [Root exception is java.net.UnknownHostException: xxx.yyy]]
При этом хостнэйм xxx.yyy не фигурирует в настройках Spring Security (там указан ip сервера AD), единственная корреляция этого хостнэйма с настройками заключается в том, что DC=xxx,DC=yyy - это корневой DN нашего AD.

Решение

Погуглив, нашел, что ошибка PartialResultException говорит о том, что AD-сервер вернул ссылку на вышестоящий AD-лес, и с обработкой этой ссылки возникла какая-то проблема (в нашем случае, ссылка содержит неизвестный хостнэйм xxx.yyy).  Одно из возможных решений - изменить порт, по которому приложение соединяется с AD, с 389 на 3268, по этому порту AD не возвращает ссылок. Спасибо http://stackoverflow.com/questions/16412236/how-to-resolve-javax-naming-partialresultexception.

среда, 3 августа 2016 г.

SLES: пропало место на диске

Проблема

Случилась тут занятная ситуация. На сервере под SLES 11 кончилось место, df показывает, что занято 100% на корневой ФС (всего объем раздела, смонтированного как корневой - более 60Гб). Стали искать, куда же место утекло, смотрим:
du -sh /*
Видим пару папочек по 2-3Гб, остальное - копейки. Суммарно 60Гб совсем не получается, т.е. место вроде бы занято, а чем именно - не понятно.

Решение

Оказалось, в Linux есть интересная особенность, которой нет в Win. Файл может быть удален (т.е. не иметь ни одной ссылки из файловой системы), но при этом продолжать использоваться программами, которые сохранили handle от него, и, соответственно, занимать место на диске. Посмотреть такие файлы можно командой:
lsof +L1
Она как раз покажет файлы, имеющие менее 1 ссылки (т.е. 0 ссылок) в файловой системе. 
В нашем случае это оказались увесистые файлы логов одной из служб, кто-то удалил эти логи, не останавливая службу, и она продолжила их использовать. Решение - останавливаем службу, место на диске магическим образом само расчищается, и секунд через 30 уже имеем 50 свободных гигов.

вторник, 2 августа 2016 г.

SLES: добавление постоянного маршрута

Есть несколько способов, один из них:
  1. Создаем исполняемый файл в /etc/sysconfig/network/if-up.d/. Он будет выполняться при каждом поднятии интерфейса
  2. Прописываем в него команду добавления нужного маршрута, например:
    route add -host 10.1.1.2 gw 192.168.1.100

четверг, 30 июня 2016 г.

SLES 11 и внезапно пропадающий процесс WSO2 ESB

Есть SLES 11, на нем крутится WSO2 ESB 4.9.0. Периодически процесс WSO2 ESB просто пропадает, в логах самой шины никаких ошибок, как будто его просто кто-то кильнул. Возникло подозрение, что процесс киляет сама ОС из-за нехватки ОП. Идем в /var/log/warn, видим там:
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048428] java invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
...
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048618] 88800 total pagecache pages
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048619] 53685 pages in swap cache
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048620] Swap cache stats: add 8392477, delete 8338792, find 21248605/22043373
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048622] Free swap  = 0kB
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048631] Total swap = 1048572kB
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057372] 1048560 pages RAM
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057374] 35908 pages reserved
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057375] 80915 pages shared
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057376] 950754 pages non-shared

Jun 30 10:21:13 ESBint2-60141 kernel: [3866020.057588] Out of memory: Kill process 26797 (java) score 432 or sacrifice child
Jun 30 10:21:13 ESBint2-60141 kernel: [3866020.057707] Killed process 26797 (java) total-vm:3475068kB, anon-rss:2180320kB, file-rss:0kB
Сравниваем указанный в последней строчке pid со значением в файле wso2carbon.pid в директории установки WSO2 ESB - совпадает. Диагноз - шина убивается системой из-за нехватки физической памяти.

вторник, 21 июня 2016 г.

requirejs и управление браузерным кэшированием с помощью конфигурационного параметра urlArgs

Задача

В проекте, использующем requirejs, встала острая необходимость управлять кэшированием js-файлов браузером. Ситуация типичная: в приложении обновляется некоторый js-файл, но клиент продолжает пользоваться старой версией файла, т.к. браузер его закэшировал (в т.н. называемый Back/Forward Cache, или BFCache: http://www.pvsm.ru/javascript/61476). И пока кэш не просрочится, ситуация не изменится. Вот как данный случай выглядит в Firebug:


Причем данная ситуация может возникать как в продуктивном развертывании, так и при разработке / отладке, и ее нужно как-то брать под свой контроль, т.к. заставлять пользователя чистить кэш - не лучшее решение.

Решение

Конечно, управлять кэшированием можно на стороне сервера, с помощью заголовков Expires / Last-Modified / Cache-Control / Pragma. Но, во-первых, предлагаемый здесь способ более гибкий. А, во-вторых, если управлять кэшированием вы решили не на самом старте проекта, а лишь в какой-то момент его жизни после выхода в продакшен, вы никакими серверными махинациями не заставите браузер раскэшировать то, что он уже закэшировал ранее.
Итак, про requeirejs. Конфигурация requirejs имеет параметр urlArgs, позволяющий добавлять GET-аргументы к URL-ам, по которым загружаются js-файлы. Например, при задании этого параметры равным "v=1" файл formatter.js будет грузиться по URL'у formatter.js?v=1. В новых версиях requirejs есть возможность использовать функцию вместо статичной строки, но сейчас не об этом. Главное, что для браузера formatter.js и formatter.js?v=1 - различные  URL, и даже закешировав ранее файл formatter.js браузер все равно пойдет на сервер за formatter.js?v=1 (хотя, сервер, конечно, по обоим URL'ам выдаст один и тот же файл, т.к. это статичный ресурс и GET-параметры для него игнорируются).
При поиске решения я наткнулся на интересную статью: http://blog.johnnyreilly.com/2014/03/caching-and-cache-busting-with-requirejs.html. В ней приводится рекомендация по использованию параметра urlArgs для управления кэшированием, которую я успешно и применил. Автор предлагает 2 разных подхода для разработки и продуктивного развертывания проекта:
  • В процессе разработки удобно, когда вообще ничего не кэшируется, и любые правки на сервере сразу же попадают в браузер. В данном случае задаем параметр urlArgs равным "v=" +  (new Date()).getTime(). В итоге имеем при каждой загрузке страницы новые URL'ы, и браузер все js-файлы гарантированно загружает с сервера
  • В продуктивном развертывании необходим баланс между актуальностью файлов и потреблением сетевых ресурсов, так что вариант, описанный выше, не подходит: файлы на сервере меняются не часто, и на какой-то промежуток времени их все же хорошо было бы кэшировать. Здесь можно воспользоваться следующим подходом: устанавливаем параметр urlArgs  равным "v=<productVersion>", где <productVersion> - версия Вашего продукта (сайта). Т.о., при выпуске и установке в продуктив новой версии Вашего продукта все клиенты при первом обращении к сайту получат актуальные версии js-файлов, т.к. изменилась версия продукта, а, значит, и все URL-ы, по которым загружаются js-ки. Далее же, до выхода следующей версии, браузер может (и будет) спокойно кэшировать js-файлы, т.к. они гарантированно остаются неизменными.

пятница, 22 апреля 2016 г.

Linux: подсчет числа потоков в приложении

Задача

Необходимо узнать, сколько потоков имеет на текущий момент то или иное запущенное приложение.

Решение
ps -eLf | grep <app_name> | wc -l
 , где <app_name> может быть именем исполняемого файла или любой другой строкой, которая может отличить интересующий процесс от других в выводе команды ps.

среда, 13 апреля 2016 г.

PostgreSQL: unrecognized configuration parameter "row_security" при восстановлении из SQL-дампа

Проблема

Сняли дамп с БД в текстовом формате, пытаемся восстановить, получаем ошибку:
ERROR: unrecognized configuration parameter "row_security"
SQL-состояние: 42704
Версия СУБД, на которую накатывался дамп, совпадает с версией СУБД, откуда он снимался.

Решение

Если посмотреть внимательно на sql-ник дампа, в начале можно заметить строки:
-- Dumped from database version 9.4.4
-- Dumped by pg_dump version 9.5.0
, говорящие о том, что дамп снимался версией pg_dump 9.5.0. Она и добавила в дамп строчку:
SET row_security = off;
, не поддерживаемую СУБД 9.4.4. Удаляем эту строчку - дамп успешно накатывается.

понедельник, 11 апреля 2016 г.

Java: подмена HTTP-заголовка, выдаваемого сервлетом / JSP-страницей

Задача

Есть некий сервлет (или JSP-страница), недоступный для модификации. Необходимо подменить один из выдаваемых сервлетом HTTP-заголоков. Например, сервлет выдает для скачивания некий файл. Необходимо подменить имя этого файла, т.е. заголовок "Content-Disposition".

Решение

Делаем jsp-страничку, которая будет служить своего рода прокси-объектом для оригинального сервлета, в ней делаем следующее:
<%
javax.servlet.http.HttpServletResponseWrapper wrappedResponse = new javax.servlet.http.HttpServletResponseWrapper(response) {
public void setHeader(String name, String value) {
if(name.equals("My-Header")) {
value = "my_value";
}
super.setHeader(name, value);
} public void addHeader(String name, String value) {
if(name.equals("My-Header")) {
value = "my_value";
}
super.addHeader(name, value);
} };
request.getRequestDispatcher("MyServlet").forward(request, wrappedResponse);
%>
Т.е. заворачиваем объект оригинального HTTP-ответа во wrapper, переопределяем методы установки заголовков, после чего выполняем forward на целевой сервлет. В итоге значение заголовка HTTP-заголовка "My-Header", генерируемого сервлетом, развернутым по относительному пути "MyServlet", будет заменено на "my_value".

среда, 6 апреля 2016 г.

Birt: объединение по вертикали ячеек таблицы

Задача

Есть таблица с данными, сгруппированными по определенному полю. Причем в рамках группы значения ячеек нескольких колонок одинаковы. В итоге таблица выглядит примерно следующим образом:
Необходимо ячейки первых двух колонок, входящие в одну группу (и содержащие одинаковые значения) объединить по вертикали.

Решение

Решение вышло несколько нетривиальным. Birt умеет убирать дублирующиеся значения к ячейках колонки (путем установки свойства "Suppress duplicates"), но он при этом не объединяет ячейки, а просто делает все ячейки под первой ячейкой с определенным значением пустыми. Это решение не подходит для случая, когда значения ячеек длинные, т.к. тогда первая строка растягивается по высоте, чего не произошло бы, будь ячейки объединены:
В итоге пришлось задействовать скрипты:
  1. На загловочную строку группы вешаем обработчик onCreate со следующим содержимым:
    reportContext.setGlobalVariable("GROUP_ROW_CNT", 0);
    Т.е. мы устанавливаем глобальную переменную GROUP_ROW_CNT в 0. Эта переменная будет счетчиком строк в рамках группы.
  2. На ячейки первого и второго столбца также вешаем обработчик onCreate:
    var curGroupRowCnt = reportContext.getGlobalVariable("GROUP_ROW_CNT");
    curGroupRowCnt++;
    reportContext.setGlobalVariable("GROUP_ROW_CNT", curGroupRowCnt);
    if(curGroupRowCnt != 1) {
    this.getStyle().display = "none";
    } else {
    this.rowSpan = 1000; // заведомо большое число, т.к. мы на данном этапе не знаем, сколько строк в группе
    }
    Т.е. мы инкрементируем счетчик строк в группе, и далее, если строка первая - устанавливаем для нее значение rowSpan, заведомо большее, чем возможное число строк в группе, а если строка не первая - то скрываем ее.
Вуаля, получаем в точности то, что хотели:

Спасибо http://birtworld.blogspot.ru/2010/10/birt-duplicate-rows.html, подтолкнул в правильном направлении.

вторник, 5 апреля 2016 г.

Birt: отладка отчета и ошибка "Failed to initialize emitter ... Access is denied"

Проблема

Пытаюсь запустить отладку birt-отчета в Birt Report Designer 4.5, получаю ошибку в логе:
апр 05, 2016 9:34:32 PM org.eclipse.birt.report.engine.api.impl.EngineTask handleFatalExceptions
SEVERE: An error happened while running the report. Cause:
org.eclipse.birt.report.engine.api.EngineException: Failed to initialize emitter.
at org.eclipse.birt.report.engine.emitter.EmitterUtil.getOuputStream(EmitterUtil.java:82)
...
Caused by: java.io.FileNotFoundException: \Myreport.rptdesign.html (Access is denied)
at java.io.FileOutputStream.open0(Native Method)
...
Работаю в Windows 7.

Решение

Зайти в конфигурацию отладки (Run -> Debug Configurations) и сменить путь в поле "Temp folder" на существующую доступную для записи директорию. Применить изменения. 

четверг, 25 февраля 2016 г.

Maven 3.3.9 и плагины maven-invoker-plugin:1.8 / exec-maven-plugin:1.2

Проблема

При попытке сборки проекта, использующего exec-maven-plugin версии 1.2, с помощью maven версии 3.3.9 возникает ошибка:
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.2:exec (def
ault) on project *****: Command execution failed. Cannot run program
 "mvn" (in directory "D:\***"): CreateProcess error=2, Не удается найти указанный файл ->
 [Help 1]
Аналогичная проблема с плагином maven-invoker-plugin версии 1.8 под maven 3.3.9:
[INFO] --- maven-invoker-plugin:1.8:run (default) @ **** ---
[INFO] Building: pom.xml
[INFO] ..FAILED (0.0 s)
[INFO]   Maven invocation failed. Error configuring command-line. Reason: Maven
executable not found at: C:\Program Files\apache-maven-3.3.9\bin\mvn.bat
При этом версией maven 3.0.4 проект собирался без проблем.

Решение

Поднять версию exec-maven-plugin до 1.4.0, а maven-invoker-plugin - до 2.0.0.

вторник, 16 февраля 2016 г.

WSO2 ESB: wso2-esb-sequence-plugin и заглавные буквы "И"

wso2-esb-sequence-plugin версии 2.0.х, где x>5, при сборке бьет кодировку, превращая заглавные буквы "И" в сиквенсах в кракозябры. Workaround'а, кроме отката на 2.0.4, пока не нашел.

вторник, 2 февраля 2016 г.

PostgreSQL: просмотр пользовательских типов через pgAdmin

pgAdmin умеет показывать пользовательские типы, но эта функциональность по-умолчанию почему-то выключена в настройках. Идем в Файл -> Параметры -> Браузер -> Показать, ставим галочку напротив "Типы", обновляем браузер объектов и вуаля:


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

ActiveMQ 5.7.0: отключение Dead Letter Queue для определенной очереди

Задача

Для определенной очереди (пусть это будет "MyQueue") отключить использование Dead Letter Queue.

Решение

В activemq.xml добавить <discardingDLQBrokerPlugin dropOnly="MyQueue" dropAll="false"/> в блок <plugins/>. Перезапустить ActiveMQ.
ЗЫ Важно указывать dropAll="false", т.к. по-умолчанию он установлен в true и имеет приоритет над dropOnly.

среда, 27 января 2016 г.

Spring Boot: No qualifying bean of type [org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder] found

Проблема

Перевели проект с spring boot 1.2.6 на 1.3.1. При старте приложения начала падать ошибка:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xxxEntityManagerFactory' defined in class xxx.xxx: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder]: : No qualifying bean of type [org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at ...
Решение

В Spring Boot 1.3.1 вместо 
org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder
нужно использовать 
org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder.

среда, 20 января 2016 г.

Виртуальный съемный диск под Windows

Бывают ситуации, когда нужен виртуальный съемный диск, например, чтобы использовать его в качестве контейнера закрытого ключа шифрования. Для этих целей подойдет бесплатная утилита ImDisk: http://reboot.pro/files/file/284-imdisk-toolkit/. Чтобы монтируемый этой утилитой диск был съемным, необходимо использовать ключ "-o rem" (либо при использовании ImDisk через консоль, либо в графическом интерфейсе конфигуратора на вкладке "Advanced" в поле "Additional parameters").

Директория HDImageStore КриптоПРО JCP

Windows

Контейнеры, хранящиеся в HDImagesStore КриптоПРО JCP физически лежат на диске в подпапке \AppData\Local\Crypto Pro домашней папки пользователя (путь можно изменить через ControlPane).

Linux

/var/opt/cprocsp/keys/<username>

Актуально для КриптоПРО JCP 2.0.38481

вторник, 19 января 2016 г.

Java: SimpleDateFormat и парсинг миллисекунд

Проблема

Есть строка с датой: "2016-01-01T11:05:42.1". Если ее пытаться распарсить, используя java.text.SimpleDateFormat и шаблон "yyyy-MM-dd'T'HH:mm:ss.S", в полученной дате мы получим 1 миллисекунду вместо 100. SimpleDateFormat всегда обрабатывает число после точки как количество миллисекунд вне зависимости от реального веса того разряда, на котором расположено число. Т.е. для SimpleDateFormat даты "2016-01-01T11:05:42.1", "2016-01-01T11:05:42.01" и  "2016-01-01T11:05:42.001" идентичны.

Решение

Варианта 2:
  1. Дополняем миллисекунды в исходной строке до трех разрядов
  2. Отказываемся от использования SimpleDateFormat в пользу сторонней библиотеки, например, Joda-Time (http://www.joda.org/joda-time/)
Для последнего вариант парсинг будет выглядеть следующим образом:
import java.util.Date;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

DateTimeFormatter format = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.S");
Date parsedValue = format.parseDateTime("2016-01-01T11:05:42.1").toDate();

понедельник, 18 января 2016 г.

JDBC, MS SQL Server и выполнение нескольких SQL-выражений одновременно

Проблема

Пытаюсь выполнить PreparedStatement, состоящий из 3 выражений:
DECLARE @myVar MyTableType;
INSERT INTO @myVar(a,b) VALUES(1,2);
EXECUTE someProc @myVar;
Т.е. объявляется переменная пользовательского табличного типа, в нее вставляется строка данных, после чего эта переменная передается в некоторую процедуру. 
Выполнение PreparedStatement производится методом execute().
Процедура внутри себя содержит инструкцию RAISERROR, которая в некоторых ситуациях генерирует ошибку. Проблема заключается в том, что если перед вызовом процедуры нет INSERT-выражения, RAISERROR приводит к появлению SQLException при выполнении execute(). Если же INSERT присутствует, то RAISERROR не приводит к SQLException в вызывающем коде, и execute() выполняется без ошибок.

Причина

Данное PreparedStatement генерирует несколько результатов, поскольку содержит несколько генерирующих результат выражений внутри себя (конструкция DECLARE не считается). Первый результат - это результат выполнения INSERT'а в переменную, который выполняется успешно, поэтому execute() не генерирует исключение, т.к. "курсор" установлен на первом результате. При попытке же сделать getMoreResults() для объекта PreparedStatement мы как раз и ловим ожидаемый SQLException, т.к. выполняется переход к результату выполнения команды EXECUTE, а этот EXECUTE у нас выполнился с ошибкой.