MarkLogic Java API обнаружение взаимоблокировок

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

К моему удивлению, marklogic java api повторяет неудачные запросы (например, из-за тупика). Это может иметь смысл, если ваш запрос не является запросом с несколькими операторами, но в противном случае я не уверен, что это так.

Итак, давайте придерживаться этой проблемы тупика. Я создал простой фрагмент кода, в котором я специально создал тупик. Фрагмент создает документ test.xml а затем пытается читать и писать из двух разных транзакций, каждая в новом потоке.

public static void main(String[] args) throws Exception {
        final Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        final Logger ok = (Logger) LoggerFactory.getLogger(OkHttpServices.class);
        root.setLevel(Level.ALL);
        ok.setLevel(Level.ALL);

        final DatabaseClient client = DatabaseClientFactory.newClient("localhost", 8000, new DatabaseClientFactory.DigestAuthContext("username", "password"));

        final StringHandle handle = new StringHandle("<doc><name>Test</name></doc>")
            .withFormat(Format.XML);
        client.newTextDocumentManager().write("test.xml", handle);

        root.info("t1: opening");
        final Transaction t1 = client.openTransaction();
        root.info("t1: reading");
        client.newXMLDocumentManager()
            .read("test.xml", new StringHandle(), t1);

        root.info("t2: opening");
        final Transaction t2 = client.openTransaction();
        root.info("t2: reading");
        client.newXMLDocumentManager()
            .read("test.xml", new StringHandle(), t2);

        new Thread(() -> {
            root.info("t1: writing");
            client.newXMLDocumentManager().write("test.xml", new StringHandle("<doc><t>t1</t></doc>").withFormat(Format.XML), t1);
            t1.commit();
        }).start();

        new Thread(() -> {
            root.info("t2: writing");
            client.newXMLDocumentManager().write("test.xml", new StringHandle("<doc><t>t2</t></doc>").withFormat(Format.XML), t2);
            t2.commit();
        }).start();

        TimeUnit.MINUTES.sleep(5);

        client.release();
    }

Этот код создаст следующий журнал:

14:12:27.437 [main] DEBUG c.m.client.impl.OkHttpServices - Connecting to localhost at 8000 as admin
14:12:27.570 [main] DEBUG c.m.client.impl.OkHttpServices - Sending test.xml document in transaction null
14:12:27.608 [main] INFO  ROOT - t1: opening
14:12:27.609 [main] DEBUG c.m.client.impl.OkHttpServices - Opening transaction
14:12:27.962 [main] INFO  ROOT - t1: reading
14:12:27.963 [main] DEBUG c.m.client.impl.OkHttpServices - Getting test.xml in transaction 5298588351036278526
14:12:28.283 [main] INFO  ROOT - t2: opening
14:12:28.283 [main] DEBUG c.m.client.impl.OkHttpServices - Opening transaction
14:12:28.286 [main] INFO  ROOT - t2: reading
14:12:28.286 [main] DEBUG c.m.client.impl.OkHttpServices - Getting test.xml in transaction 8819382734425123844
14:12:28.289 [Thread-1] INFO  ROOT - t1: writing
14:12:28.289 [Thread-1] DEBUG c.m.client.impl.OkHttpServices - Sending test.xml document in transaction 5298588351036278526
14:12:28.289 [Thread-2] INFO  ROOT - t2: writing
14:12:28.290 [Thread-2] DEBUG c.m.client.impl.OkHttpServices - Sending test.xml document in transaction 8819382734425123844

ни t1 или же t2 будет совершено. Журналы MarkLogic подтверждают, что фактически существует тупик:

==> /var/opt/MarkLogic/Logs/8000_AccessLog.txt <==
127.0.0.1 - admin [24/Nov/2018:14:12:30 +0000] "PUT /v1/documents?txid=5298588351036278526&category=content&uri=test.xml HTTP/1.1" 503 1034 - "okhttp/3.9.0"

==> /var/opt/MarkLogic/Logs/ErrorLog.txt <==
2018-11-24 14:12:30.719 Info: Deadlock detected locking Documents test.xml

Это не будет проблемой, если один из запросов завершится неудачно и выдаст исключение, но это не так. MarkLogic Java Api повторяет каждый запрос до 120 секунд, а время ожидания обновления составляет примерно 120 секунд:

Exception in thread "Thread-1" com.marklogic.client.FailedRequestException: Service unavailable and maximum retry period elapsed: 121 seconds after 65 retries
    at com.marklogic.client.impl.OkHttpServices.putPostDocumentImpl(OkHttpServices.java:1422)
    at com.marklogic.client.impl.OkHttpServices.putDocument(OkHttpServices.java:1256)
    at com.marklogic.client.impl.DocumentManagerImpl.write(DocumentManagerImpl.java:920)
    at com.marklogic.client.impl.DocumentManagerImpl.write(DocumentManagerImpl.java:758)
    at com.marklogic.client.impl.DocumentManagerImpl.write(DocumentManagerImpl.java:717)
    at Scratch.lambda$main$0(scratch.java:40)
    at java.lang.Thread.run(Thread.java:748)

Каковы возможные способы преодоления этой проблемы? Одним из способов может быть установка максимального времени жизни для транзакции (например, 5 секунд), но это кажется хакерским и ненадежным. Есть другие идеи? Есть ли другие настройки, которые я должен проверить?

Я на MarkLogic 9.0-7.2 и используя marklogic-client-api:4.0.3,

Изменить: Одним из способов устранения тупика было бы путем синхронизации вызывающей функции, это на самом деле так, как я решил ее в моем случае (см. Комментарии). Но я думаю, что основная проблема все еще существует. Наличие тупика в транзакции с несколькими выписками не должно быть скрыто за 120 секунд. Скорее всего, у меня сразу же неудачный запрос, а не 120-секундная блокировка для одного из моих документов + 64 неудачных попытки на поток.

1 ответ

Решение

Тупиковые ситуации обычно устраняются повторной попыткой. Внутренне сервер выполняет цикл внутренней повторной попытки, потому что обычно взаимоблокировки являются временными и случайными, длящимися очень короткое время. В вашем случае вы создали случай, который никогда не будет успешным с любым временем ожидания, равным для обоих потоков. На прикладном уровне можно избежать взаимных блокировок, избегая транзакций с несколькими операторами при использовании REST API. (это то, что использует Java API). Многопоточные транзакции по REST не могут быть реализованы на 100% безопасно из-за ответственности клиента за управление идентификатором транзакции и неспособности сервера обнаруживать ошибки на стороне клиента или идентификацию на стороне клиента. Очень тонкие проблемы могут возникать и случаются, если вы не проявляете агрессивную активность при обработке ошибок и многопоточности. Если вы "протолкнете" логику на сервер (xquery или javascript), сервер сможет намного лучше управлять всем.

Что же касается, если его "хорошо" или нет для Java API для реализации повторных попыток для этого случая, это спорно так или иначе. (Компромисс для, казалось бы, простого в использовании интерфейса заключается в том, что многие вещи, которые в противном случае были бы опциями, решаются для вас как соглашение. Как правило, нет единого универсального ответа. В этом случае я предполагаю, что мысль была что тупик, скорее всего, вызван независимым кодом / логикой 'аварией', а не идентичным кодом, работающим по касательной - повторная попытка в этом случае будет хорошим выбором. В вашем примере это не так, но тогда более ранняя ошибка все равно будет предсказуемо провалиться, пока вы не измените свой код на "не делайте этого").

Если он еще не существует, запрос функции для настраиваемого тайм-аута и поведения повтора кажется разумным запросом. Однако я бы порекомендовал попытаться избежать любых вызовов REST, которые приводят к открытой транзакции, что по своей сути проблематично, особенно если вы не замечаете проблему заранее (тогда она с большей вероятностью укушает вас в работе). В отличие от JDBC, который сохраняет соединение открытым, чтобы сервер мог обнаруживать разъединения клиента, HTTP и ML Rest API этого не делают - что приводит к другой модели программирования, чем традиционное кодирование базы данных в Java.

Другие вопросы по тегам