Сбой изоляции транзакции XG во время теста, который генерирует уникальный порядковый номер (Objectify4)

Мне нужно сгенерировать уникальный номер счета-фактуры в транзакции XG, которая включает следующие 3 группы сущностей в моей модели данных:

  • (верхний уровень) ContactRoot ‹-- (предок) ‹--- Контакт: контакт должен быть обновлен до статуса Клиент во время транзакции

  • (верхний уровень) CustomerSettings: содержит следующий порядковый номер для использования; существует один и только один экземпляр CustomerSettings с фиксированным статическим идентификатором; порядковый номер должен быть увеличен на +1 во время транзакции

  • (верхний уровень) InvoiceRoot ‹-- (предок) ‹--- Invoice: присвоить новый уникальный номер счета на основе порядкового номера в CustomerSettings;

Это важная часть реализации DAO (удалены ненужные проверки бизнес-правил и т. д.):

public void saveInvoice(final Invoice invoice) throws BusinessRuleException {

    final Objectify ofy = ObjectifyService.factory().begin().cache(true);
    ofy.transact(new Work<Void>() {

        @Override
        public Void run() {
            CustomerSettings customerSettings = ofy.load()
                    .key(Key.create(CustomerSettings.class, CustomerSettings.ID)).safeGet();
            Contact contact = ofy.load().key(createContactKey(invoice.getContactId()).safeGet();
            contact.setContactType(ContactType.CLIENT);
            ofy.save().entity(contact).now();
            String invoiceNumber = generateSequence(ofy, customerSettings);
            invoice.setInvoiceNumber(invoiceNumber);
            ofy.save().entity(invoice).now();
            return null;
        }
    });
}

И упрощенная версия для создания следующего порядкового номера, где следующий порядковый номер увеличивается для следующего вызова, а CustomerSettings должны быть транзакционно обновлены (у меня это синхронизировано, но я думаю, что это не очень полезно):

private synchronized String generateSequence(Objectify ofy, CustomerSettings settings) {
    String ret = "";
    int sequence = settings.getNextSequence();
    settings.setNextSequence(sequence + 1);
    ofy.save().entity(settings).now();
    ret = "" + sequence;
    return ret;
}

Вот как выглядит мой модульный тест для переменного количества потоков:

private void test(final int threadCount) throws InterruptedException, ExecutionException {
    final Environment currentEnvironment = ApiProxy.getCurrentEnvironment();
    Callable<String> task = new Callable<String>() {
        @Override
        public String call() {
            ApiProxy.setEnvironmentForCurrentThread(currentEnvironment);
            return generateInvoiceNumber();
        }
    };
    List<Callable<String>> tasks = Collections.nCopies(threadCount, task);
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
    List<Future<String>> futures = executorService.invokeAll(tasks);
    List<String> resultList = new ArrayList<String>(futures.size());
    // Check for exceptions
    for (Future<String> future : futures) {
        // Throws an exception if an exception was thrown by the task.
        resultList.add(future.get());
    }
    // Validate the IDs
    Assert.assertEquals(futures.size(), threadCount);
    List<String> expectedList = new ArrayList<String>(threadCount);
    for (long i = 1; i <= threadCount; i++) {
        expectedList.add("" + i);
    }
    Collections.sort(resultList);
    Assert.assertEquals(expectedList, resultList);
}

@SuppressWarnings("unchecked")
private String generateInvoiceNumber() {
    InvoiceDAO invoiceDAO = new InvoiceDAO();
    Invoice invoice = ... create a valid invoice
    invoiceDAO.saveInvoice(invoice);
    log.info("generated invoice number : " + invoice.getInvoiceNumber());
    return invoice.getInvoiceNumber();
}

например, когда я запускаю это с 32 одновременными потоками:

@Test
public void test32() throws InterruptedException, ExecutionException {
    test(32);
}

Но последующие потоки не видят, что предыдущая транзакция увеличила последовательность номеров счетов.

Это результат:

junit.framework.AssertionFailedError: ожидается:‹[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]> но было:‹[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3]>

Я уже пару раз просмотрел документы, не могу понять, почему это не работает?

Если вы получаете доступ к более чем одной группе сущностей в транзакции, транзакция будет транзакцией XG. Если вы делаете доступ только к одному, это не так. Стандартный лимит в 5 EG применяется ко всем транзакциям. документация по транзакциям

Что я делаю неправильно ?


comment
добавлена ​​дополнительная информация в группу google для объективации: groups.google .com/forum/?fromgroups=#!topic/objectify-appengine/   -  person koma    schedule 30.10.2012
comment
Есть ли причина, по которой вы не используете хранилище данных для создания следующего номера счета в качестве идентификатора объекта? У вас есть какое-то требование, чтобы идентификатор был old-id + 1? Если бы вы могли ослабить это требование, вы могли бы полностью избавиться от #generateSequence.   -  person sappenin    schedule 02.12.2012
comment
да, это номера счетов-фактур, юридические требования должны быть последовательными;   -  person koma    schedule 03.12.2012


Ответы (1)


Этот фрагмент кода делает код не транзакционным:

окончательная Objectify ofy = ObjectifyService.factory().begin().cache(true);

ofy.transact(new Work<Void>() {

       ....
       ofy.save().entity(settings).now();   
       .... 

}

потому что я повторно использую объектный экземпляр, который не является транзакционным. Чтобы получить экземпляр внутри транзакции Work, вы всегда должны запрашивать экземпляр следующим образом:

ObjectifyService.ofy()

дополнительную информацию можно найти в групповом обсуждении здесь.

Глядя на реализацию ObjectifyService, вы можете видеть, что новые экземпляры помещаются/извлекаются в/из стека;

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

person koma    schedule 31.10.2012