在生成唯一序列号的测试期间,XG事务隔离失败(Objectify4)

时间:2012-10-22 20:45:07

标签: multithreading google-app-engine transactions google-cloud-datastore objectify

我必须在XG交易中生成一个唯一的发票号,其中包含以下数据模型中的 3个实体组

  • (toplevel)ContactRoot< - (祖先)< --- 联系:联系必须在交易期间更新为状态客户

  • (toplevel) CustomerSettings :保存要使用的下一个序列号; CustomerSettings只有一个实例,具有固定的静态ID;在交易期间,序列号必须增加+1

  • (toplevel)InvoiceRoot< - (祖先)< --- 发票:根据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:expected:&lt; [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]&gt;但是:&lt; [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,3,3,3,3,3,2,1,2,2,2,2,2,2,2,2,2,3,3,3,3   3]&GT;

我已经浏览了几次文档,无法弄清楚为什么这不起作用?

  

如果您在交易中访问多个实体组,则   交易是一个XG交易。如果你只访问一个,它   不是。 5个EG的标准限制适用于所有交易。   objectify transactions documentation

我做错了什么?

1 个答案:

答案 0 :(得分:1)

这段代码使代码不是事务性的:

  

最终Objectify ofy =   。ObjectifyService.factory()()开始高速缓冲存储器(真);

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

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

}

因为我重用了非事务性的objectify实例。要在事务处理中获取实例,您必须始终询问这样的实例:

  

ObjectifyService.ofy()

小组讨论here中的更多信息。

查看ObjectifyService的实现,您可以看到新实例被推入/弹出堆栈;

除此之外,测试用例仍然没有运行..测试的最佳方法可能是发送http请求同时发送;