понедельник, 28 сентября 2020 г.

Компания "ALL-Vision": видеонаблюдение и СКУД в Иркутске

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

Сайт организации и интернет-магазин: https://all-vision-irk.ru/

суббота, 19 сентября 2020 г.

Решение проблемы с зависанием веб-приложения

Пример анализа и, надеюсь, решения проблемы с зависаниями веб-приложения.

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

Единственной полезной зацепкой была ошибка в "catalina.out" (лог-файл веб-сервера Apache Tomcat 9, при чем не сразу стало понятно, что указанная ниже информация записывается туда, при штатных настройках Apache Tomcat, только после остановки веб-сервера или после остановки проблемного веб-приложения, например, в "Tomcat Manager Application"):
19-Sep-2020 08:26:57.691 WARNING [http-nio-8080-exec-914] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [jenki##2.1.28] is still processing a request that has yet to finish. This is very likely to create a memory leak. You can control the time allowed for requests to finish by using the unloadDelay attribute of the standard Context implementation. Stack trace of request processing thread:[
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
org.apache.tomcat.dbcp.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:586)
org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:438)
org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:359)
org.apache.tomcat.dbcp.dbcp2.PoolingDataSource.getConnection(PoolingDataSource.java:134)
org.apache.tomcat.dbcp.dbcp2.BasicDataSource.getConnection(BasicDataSource.java:1543)
com.jenki.multicon.AppContextReader.<init>(AppContextReader.java:14)
com.jenki.LoginServlet.doPost(LoginServlet.java:46)
...
Последние две строки дали мне, как автору веб-приложения, понять, что проблема возникла, когда пользователь ввел пароль и началось ожидание подключения к БД для загрузки данных, которые пользователь ожидает на экране своего интернет-браузера. Таким образом, это дало уверенность в том, что на найденную ошибку можно спокойно опереться при дальнейшем анализе.

Поиск по остальным строкам стека ошибки привел к подобного рода обсуждениям: https://github.com/dotCMS/core/issues/7790 - тут описываются нюансы настройки подключений к БД через "context.xml" Томката (как раз, мой случай) с использованием параметров:
maxActive="60" maxIdle="10" maxWait="60000" removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" validationQuery="SELECT 1" testOnBorrow="true"
После попытки применения указанных на странице по ссылке параметров "maxActive", "maxWait" Томкат при запуске в своей консоли сообщает о том, что эти параметры для "фабрики пула соединений" устарели и вместо них нужно использовать актуальные, соответственно, "maxTotal" (по умолчанию "8" - Привет, зависания после 8 обновлений веб-страницы!), "maxWaitMillis" (по умолчанию "-1"). Там же Томкат сообщает наименование текущей фабрики подключений - "org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory" - оказывается, эта фабрика запускается Томкатом 9 автоматически, если администратор в context.xml в блоке настроек для подключения к БД не указывает другую фабрику с помощью тэга "factory" (описано в https://tomcat.apache.org/tomcat-9.0-doc/config/context.html#Resource_Definitions).

Поиск дополнительной информации на тему зависаний веб-приложения после 8 подключений к БД приводит к совету https://stackoverflow.com/questions/30879706/why-does-my-tomcat-only-open-8-jdbc-connections об использовании «factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"».

В итоге, блок настройки подключений к БД веб-приложения выглядит следующим образом:
    <Resource name="jdbc/jenki"
        auth="Container"
        driverClassName="oracle.jdbc.driver.OracleDriver"
        url="jdbc:oracle:thin:@myserver:1521:db"
        username="mySYSDBA"
        password="mypassw"
        type="javax.sql.DataSource"
        factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
        maxActive="100" maxIdle="30" maxWait="10000"
        removeAbandoned="true" removeAbandonedTimeout="40" logAbandoned="true" testOnBorrow="true" />

Теперь в логе Томката (catalina.out) видны лишь строки, символизирующие закрытие подключений (позднее будет проведена проверка кода приложения - возможно, подключения не всегда корректно закрываются, но пока с этим справляется "Tomcat JDBC Pool Cleaner"):
23-Sep-2020 12:27:26.355 WARNING [Tomcat JDBC Pool Cleaner[2129789493:1600853171352]] org.apache.tomcat.jdbc.pool.ConnectionPool.abandon Connection has been abandoned PooledConnection[oracle.jdbc.driver.T4CConnection@3af0c98d]:java.lang.Exception
...
23-Sep-2020 12:52:51.415 WARNING [Tomcat JDBC Pool Cleaner[2129789493:1600853171352]] org.apache.tomcat.jdbc.pool.ConnectionPool.abandon Connection has been abandoned PooledConnection[oracle.jdbc.driver.T4CConnection@6ea26def]:java.lang.Exception
...
23-Sep-2020 14:02:51.570 WARNING [Tomcat JDBC Pool Cleaner[2129789493:1600853171352]] org.apache.tomcat.jdbc.pool.ConnectionPool.abandon Connection has been abandoned PooledConnection[oracle.jdbc.driver.T4CConnection@41e58b1f]:java.lang.Exception
...
23-Sep-2020 14:04:26.574 WARNING [Tomcat JDBC Pool Cleaner[2129789493:1600853171352]] org.apache.tomcat.jdbc.pool.ConnectionPool.abandon Connection has been abandoned PooledConnection[oracle.jdbc.driver.T4CConnection@338e76fa]:java.lang.Exception
...
23-Sep-2020 14:52:16.686 WARNING [Tomcat JDBC Pool Cleaner[2129789493:1600853171352]] org.apache.tomcat.jdbc.pool.ConnectionPool.abandon Connection has been abandoned PooledConnection[oracle.jdbc.driver.T4CConnection@2165cd9c]:java.lang.Exception
===========================
Порядок действий для стабильного повторения:
1. Открываем несколько разных сессий к веб-приложению (можно даже под одним и тем же пользователем, главное, чтобы это были разные сессии), для этого нужно, к примеру, если тестируется на одном компьютере, то можно открыть 6 окон в 3-х разных браузерах (к примеру, Яндекс.Браузер, Firefox, Chrome): 3 окна в штатном режиме и 3 окна в приватном;
2. Несколько раз (около 6 в каждом окне по очереди) обновляем страницу;
3. Ждем около 20-30 минут (окна браузеров не закрываем);
4. Повторяем пункт 2.

Ожидаемый результат (до исправления):
обновление страницы будет выполнено штатно, пользователь увидит данные на обновленной странице.

Фактический результат (до исправления):
Примерно 4 из 6 окон перейдут в режим ожидания ответа от веб-сервера.

Результат тестирования после примененного исправления ...

среда, 16 сентября 2020 г.

Зависание сервлета

Почему-то при наличии строки
String defaultServer = new AppDatabaseConnectionObject().getDefaultServer();
в классе "DataviewerServlet" веб-страница "зависает" после 8-го обновления страницы в браузере.

Причина мне пока не понятна.
AppDatabaseConnectionObject.java:
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class AppDatabaseConnectionObject {
    private Connection connection;
    private String defaultServer;

    public AppDatabaseConnectionObject(
    ) throws ClassNotFoundException, NamingException, SQLException {
        Context context = new InitialContext();
        DataSource datasource = (DataSource) context.lookup("java:/comp/env/jdbc/myapp");
        connection = datasource.getConnection();
        defaultServer = (String) context.lookup("java:/comp/env/myapp/defaultServer");
    }

    public Connection getConnection() {
        return connection;
    }

    public String getDefaultServer() {
        return defaultServer;
    }
}
DataviewerServlet.java:
import ch.qos.logback.classic.Logger;
import com.myapp.AppDatabaseConnectionObject;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class DataviewerServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            Connection con = new AppDatabaseConnectionObject().getConnection();
            String defaultServer = new AppDatabaseConnectionObject().getDefaultServer();
...

Исправил, заменив в классе "DataviewerServlet" эти строки:
Connection con = new AppDatabaseConnectionObject().getConnection();
String defaultServer = new AppDatabaseConnectionObject().getDefaultServer();
на эти:
AppDatabaseConnectionObject appdbcon = new AppDatabaseConnectionObject();
Connection con = appdbcon.getConnection();
String defaultServer = appdbcon.getDefaultServer();

вторник, 15 сентября 2020 г.

Хранение переменных в context.xml

Почему-то не смог применить пример с хранением переменной в "context.xml" в теге "Parameter" (попытка обращения завершается ошибкой "javax.naming.NameNotFoundException: Name [companyName] is not bound in this Context. Unable to find [companyName]"), зато помог тег "Environment":
<Environment name="companyName" value="My Company, Incorporated" type="java.lang.String" override="false"/>

Оба примера показаны здесь.

четверг, 30 июля 2020 г.

СЭД: The parameter is incorrect.

Если во время входа в сервис СЭД вы видите страницу с ошибкой:

Ошибка в приложении!

Для перезапуска приложения перейдите по этой ссылке!

...

jcifs.smb.SmbException: The parameter is incorrect.

Сообщите о возникновении данной ошибки Администратору Системы.

Тогда вам поможет "gpedit.msc": Конфигурация компьютера → Конфигурация Windows → Параметры безопасности → Локальные политики → Параметры безопасности → "Сетевая безопасность: уровень проверки подлинности LAN Manager" = "Отправлять LM- и NTLM ответы".

суббота, 11 июля 2020 г.

Потокобезопасность. Начало.

Мое знакомство с многопоточностью.

Плохой пример:
public class PingServlet extends HttpServlet {
    private List OutputList = new ArrayList<>();
    private List OutputErrorList = new ArrayList<>();
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        OutputList.clear();
        OutputErrorList.clear();
        try {
            String ip = request.getParameter("ip");
            InetAddress host = InetAddress.getByName(ip);
            int ping_count = 4;
            int ping_timeout_seconds = 2;
            int ping_timeout_milliseconds = ping_timeout_seconds * 1000;
            int ping_ttl = 54;
            OutputList.add("Обмен " + ping_count + " пакетами с timeout " + ping_timeout_seconds + " сек. и TTL " + ping_ttl + ".");
            int i = 1;
            while (i <= ping_count) {                 if (host.isReachable(null, ping_ttl, ping_timeout_milliseconds)) {                     OutputList.add(ip + ": ОК.");                 } else {                     OutputList.add(ip + ": узел недоступен.");                 }                 i++;             }         } catch (Exception e) {             e.printStackTrace();             OutputErrorList.add(e.toString());         }         response.setContentType("text/html; charset=UTF-8"); // Для правильной обработки кириллицы.         if (request.getCharacterEncoding() == null) { // Без этого неправильно выполнялась обработка ввода с кирилицей.             request.setCharacterEncoding("UTF-8");         }         request.getSession().setAttribute("servlet_data_ping", OutputList);         request.getSession().setAttribute("servlet_error_ping", OutputErrorList);         request.getRequestDispatcher("ping.jsp").include(request, response);     } }

Внимание на эти строки:



При такой реализации одновременное выполнение в нескольких потоках приводит к смешиванию значений из нескольких потоков в одной переменной:


Один из вариантов исправления:
public class PingServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List OutputList = new ArrayList<>();
        List OutputErrorList = new ArrayList<>();
        try {
            String ip = request.getParameter("ip");
            InetAddress host = InetAddress.getByName(ip);
            int ping_count = 4;
            int ping_timeout_seconds = 2;
            int ping_timeout_milliseconds = ping_timeout_seconds * 1000;
            int ping_ttl = 54;
            OutputList.add("Обмен " + ping_count + " пакетами с timeout " + ping_timeout_seconds + " сек. и TTL " + ping_ttl + ".");
            int i = 1;
            while (i <= ping_count) {                 if (host.isReachable(null, ping_ttl, ping_timeout_milliseconds)) {                     OutputList.add(ip + ": ОК.");                 } else {                     OutputList.add(ip + ": узел недоступен.");                 }                 i++;             }         } catch (Exception e) {             e.printStackTrace();             OutputErrorList.add(e.toString());         }         response.setContentType("text/html; charset=UTF-8"); // Для правильной обработки кириллицы.         if (request.getCharacterEncoding() == null) { // Без этого неправильно выполнялась обработка ввода с кирилицей.             request.setCharacterEncoding("UTF-8");         }         request.getSession().setAttribute("servlet_data_ping", OutputList);         request.getSession().setAttribute("servlet_error_ping", OutputErrorList);         request.getRequestDispatcher("ping.jsp").include(request, response);     }     protected void doPost(HttpServletRequest request, HttpServletResponse response) {     } }


Внимание на эти строки:

четверг, 14 мая 2020 г.

Доступ к СУБД Firebird 3.0 через JSP под Tomcat

Пример доступа к БД Firebird 3.5 через JSP под Tomcat.

Чтобы научить Apache Tomcat 9 под Java 1.8 подключаться к базам Firebird 3.0, копируем из архива jaybird-4.0.0.java8.zip файлы jaybird-full-4.0.0.java8.jar, lib\antlr-runtime-4.7.2.jar, lib\connector-api-1.5.jar, lib\jna-5.5.0.jar в папку <tomcat>\lib.
Создаем web-приложение: JSP-страница, которая будет выводить содержимое таблицы "USER" из БД "c:/data/base.fdb" сервера "dbserver"
<tomcat>\webapps\test_app\index.jsp

Листинг index.jsp:
<%@page import="java.sql.*" %>
<%Class.forName("org.firebirdsql.jdbc.FBDriver"); %>
<HTML>
    <HEAD>
        <TITLE>Database access through JSP</TITLE>
    </HEAD>
    <BODY>
        <H1>Result of select query</H1>
        <%
            Connection connection = DriverManager.getConnection(
                "jdbc:firebirdsql:dbserver/3255:c:/data/base.fdb",
                "SYSDBA",
                "masterkey");
            Statement statement = connection.createStatement();
            ResultSet resultset = statement.executeQuery("select id, name from user order by id asc");
        %>
        <TABLE BORDER="1">
            <TR>
                <TH>id</TH>
                <TH>name</TH>
            </TR>
            <% while(resultset.next()){ %>
            <TR>
                <TD> <%= resultset.getString(1) %></TD>
                <TD> <%= resultset.getString(2) %></TD>
            </TR>
            <% } %>
        </TABLE>
    </BODY>
</HTML>

вторник, 18 февраля 2020 г.

Четверги

27.02.2020
12.03.2020
26.03.2020
09.04.2020
23.04.2020
07.05.2020
21.05.2020
04.06.2020
18.06.2020
02.07.2020
16.07.2020
30.07.2020
13.08.2020
27.08.2020
10.09.2020
24.09.2020
08.10.2020
22.10.2020
05.11.2020
19.11.2020
03.12.2020
17.12.2020
31.12.2020

суббота, 15 февраля 2020 г.

Исполняемый блок с перебором значений, указанных через запятую

declare
    dbnames varchar(32767) := 'DBNAME1, DBNAME2, DBNAME3';
    dbcnt integer;
    dbnames_length integer;
    separ_position integer := 0;
    dbname varchar(32767);
begin
/* Сразу убираем лишние символы (в этом примере - пробелы) */
    dbnames := replace(dbnames,' ','');
    dbnames_length := length(dbnames);
    dbcnt := regexp_count(dbnames, ',') + 1;
    for i in 1..dbcnt loop
        separ_position := instr(dbnames,',');
        dbname := substr(dbnames, 1, separ_position - 1);
/* Если итерация последняя, то берем остаток строки без вычислений */
        if i = dbcnt then dbname := dbnames; end if;
        dbms_output.put_line(dbname);
/* Отсекаем от начала исходной переменной "отработанную" часть */        
        dbnames := substr(dbnames, separ_position + 1,dbnames_length);
    end loop;    
end;

Исполняемый блок SQL с итерациями по ID

DECLARE
    c integer := 0;
    find_s varchar(15) := 'ORGACC_IDS="';
    accstr varchar(32767);
    ac_id integer;
    acc_num varchar2(35);
    tmp1_acc_cnt integer := 0;
    o_inn varchar2(20);
    o_name varchar2(255);
CURSOR crsr is
    select
    r.id,
    r.DOC_NUMBER,
    t.CAPTION,
    INSTR(r.PARAMS,find_s)+12 as START_TAG,
    INSTR(r.PARAMS,'"',INSTR(r.PARAMS,find_s)+12+1) as END_TAG,
    ((INSTR(r.PARAMS,'"',INSTR(r.PARAMS,find_s)+12+1))-(INSTR(r.PARAMS,find_s)+12)) as TAG_LEN,
    SUBSTR(r.PARAMS,INSTR(r.PARAMS,find_s)+12,((INSTR(r.PARAMS,'"',INSTR(r.PARAMS,find_s)+12+1))-(INSTR(r.PARAMS,find_s)+12))) as ACC_STR,
    regexp_count(SUBSTR(r.PARAMS,INSTR(r.PARAMS,find_s)+12,((INSTR(r.PARAMS,'"',INSTR(r.PARAMS,find_s)+12+1))-(INSTR(r.PARAMS,find_s)+12))),',')+1 as ACC_CNT
    from PBSREPTEMPLATE r join PBSREPTYPE t on t.id=r.PBSREPTYPE_ID
    /* where r.BUDGET_ID in () */ /* where r.id in (800000001851) - для проверки на одном шаблоне */;
BEGIN
    dbms_output.put_line('ID'||chr(09)||'Номер'||chr(09)||'Тип'||chr(09)||'Счет'||chr(09)||'ИНН'||chr(09)||'Организация');
    FOR s in crsr LOOP
        accstr := s.ACC_STR;
        FOR i in 1..s.ACC_CNT LOOP /* Разбираем строку с перечислением ID счетов на отдельные части */
            c := INSTR(accstr,',');
            ac_id := SUBSTR(accstr,1,c-1);
            accstr := SUBSTR(accstr,c+1,s.TAG_LEN);
            if i = s.ACC_CNT then ac_id := accstr; end if; /* Если итерация крайняя, то выводим остаток строки с перечислением ID счетов, там как раз остается крайний счет */
            select count(*) into tmp1_acc_cnt from orgaccount where close_date is null and id = ac_id;
            if tmp1_acc_cnt = 0 then continue; end if; /* Если счет закрыт, то вывод инфы пропускаем */
            select ACCOUNT_NUMBER into acc_num from orgaccount where close_date is null and id = ac_id;
            select o.taxcode, o.caption into o_inn, o_name from org o where exists (select null from orgaccount oa where oa.ORG_ID=o.id and oa.id = ac_id);
            dbms_output.put_line(s.id||chr(09)||trim(s.DOC_NUMBER)||chr(09)||s.CAPTION||chr(09)||acc_num||chr(09)||o_inn||chr(09)||o_name);
        END LOOP;
    END LOOP;
END;

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

Скрипты бэкапа и рестора Firebird для Linux

Бэкап:
/opt/firebird/bin/gbak -user SYSDBA -password masterkey -b -v -g -Y backup_$1.log localhost/3050:`pwd`/$1 $1.FBK


Рестор:
/opt/firebird/bin/gbak -user SYSDBA -password masterkey -R -c -V -P 16384 -Y restore_$1.log $1 localhost/3050:`pwd`/R_$1.fdb

Архив