I have to generate a unique invoice number in a XG transaction that includes the following 3 entity groups in my data model :
-
(toplevel) ContactRoot <– (ancestor) <— Contact : contact must be updated to status Client during the transaction
-
(toplevel) CustomerSettings : holds the next sequence number to use; there is one and only one instance of the CustomerSettings with a fixed, static ID; the sequence number must be increased +1 during the transaction
-
(toplevel) InvoiceRoot <– (ancestor) <— Invoice : assign new unique invoice number based on the sequence number in CustomerSettings;
This is the essential part of the DAO implementation (irrelevant business rules checks etc removed) :
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;
}
});
}
And the simplified version to generate the next sequence number where the next sequence number is increased for the next call and the CustomerSettings must be transactionally updated (I have this synchronized but I guess that is not really useful)
:
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;
}
This is what my unit test looks like for a variable thread count :
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();
}
for instance when I run this with 32 threads simultaneous :
@Test
public void test32() throws InterruptedException, ExecutionException {
test(32);
}
But subsequent threads do not see that a previous transaction increased the invoice number sequence.
This is the result :
junit.framework.AssertionFailedError: expected:<[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]> but was:<[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]>
I went through the docs already a couple of times, can’t figure out why this is not working ?
If you access more than one entity group in a transaction, the
transaction with be an XG transaction. If you do access only one, it
is not. The standard limit of 5 EGs applies to all transactions.
objectify transactions documentation
what am I doing wrong ?
This piece of code make the code not transactional :
because I reuse the objectify instance that is not transactional. To get the instance inside the transaction Work, you must always ask the instance like this :
more info in the group discussion here.
Looking at the implementation of
ObjectifyService, you can see that new instances are pushed/popped onto/from a stack;Apart from that, the test case is still not running .. the best way to test is presumably firing http requests simulataneous;