EntityAutoService 'store' is not thread safe

It seems to me that the store auto service has a concurrency issue, for example if 2 REST clients call it at the same time. I was also able to reproduce this behavior in a test:

def "should not throw exception when product price already exists"() {
        when:
        for (i in 0..1) {
            new Thread (new Runnable(){
                @Override
                void run() {
                    var ec = Moqui.getExecutionContext()
                    ec.user.loginAnonymousIfNoUser()
                    ec.artifactExecution.disableAuthz()
                    System.out.println(ec.service.sync().name("store", "mantle.product.ProductPrice")
                            .parameters([productPriceId: "L1_DEMO_1_1_L2_"+i,
                                         productId: "DEMO_1_1",
                                         "otherPartyItemId": "DEMO_1_1"])
                            .call())
                    System.out.println(ec.service.sync().name("store", "mantle.product.ProductPrice")
                            .parameters([productPriceId: "L1_DEMO_1_1_L2_"+i,
                                         productId: "DEMO_1_1",
                                         "otherPartyItemId": "DEMO_1_1"])
                            .call())
                }
            }, "Thread_"+i).start()
        }
        Thread.sleep(5000)

        then:
        true

        cleanup:
        for (i in 0..1)
            ec.service.sync().name("delete#mantle.product.ProductPrice")
                    .parameters([productPriceId: "L1_DEMO_1_1_L2_"+i])
                    .call()
    }

The expected behavior is that the price is either created or updated.
The actual behavior is that if the price doesn’t exist, an exception is thrown.

13:21:13.680  WARN     Thread_0            o.moqui.i.e.EntityValueImpl Error creating [mantle.product.ProductPrice: [productPriceId:L1_DEMO_1_1_L2_1...]] tx a Bitronix Transaction with GTRID [3137322E32352E3234302E310000019AC50BC2A600000030], status=ACTIVE, 1 resource(s) enlisted (started Thu Nov 27 13:21:13 EET 2025) con Group: transactional, Con: a ConnectionJavaProxy of a JdbcPooledConnection from datasource transactional_DS in state ACCESSIBLE with usage count 1 wrapping org.postgresql.xa.PGXAConnection@4e0a6a2e on Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6802e043: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "pk_product_price"
  Detail: Key (product_price_id)=(L1_DEMO_1_1_L2_1) already exists.
13:21:13.699 ERROR     Thread_0        o.moqui.i.s.ServiceCallSyncImpl Error running service store#mantle.product.ProductPrice
org.moqui.impl.entity.EntitySqlException: Error creating ProductPrice [productPriceId:L1_DEMO_1_1_L2_1]: record already exists [23505]
	at org.moqui.impl.entity.EntityValueBase.create(EntityValueBase.java:1543) ~[moqui-framework-3.1.0-rc2.jar:3.1.0-rc2]
	at org.moqui.impl.entity.EntityValueBase.createOrUpdate(EntityValueBase.java:650)
	at org.moqui.impl.service.runner.EntityAutoServiceRunner.storeRecursive(EntityAutoServiceRunner.groovy:358)
	at org.moqui.impl.service.runner.EntityAutoServiceRunner.storeEntity(EntityAutoServiceRunner.groovy:311)
	at org.moqui.impl.service.ServiceCallSyncImpl.runImplicitEntityAuto(ServiceCallSyncImpl.java:605) 
	at org.moqui.impl.service.ServiceCallSyncImpl.callSingle(ServiceCallSyncImpl.java:215)
	at org.moqui.impl.service.ServiceCallSyncImpl.call(ServiceCallSyncImpl.java:125)
2 Likes

Semaphores are not managed at the service level, but rather with transactions at the entity/data level. For transactional operations, use transactions.

There are a few OOTB examples of this that have automated multi-thread tests for this reason, the main one that comes to mind is the inventory reservation contention test using multiple threads placing orders (along with any other data contentions that are possible for orders, but inventory reservation is the main operation that requires the same database records across multiple orders).

I guess my assumption was that the store service will only run a single instance at a given time for the same entity, which in fact is wrong. I though transaction=“use-or-begin”, which is the default value, was taking care of this, but it seems this only handles the atomically commit or rollback of all database transactions inside the service, as far as I understand.

As far as I now understand, there are 2 ways to solve concurrency issues in Moqui:

  1. Use the semaphore attribute on the service, which does exactly this, makes sure there is only one running instance of a service at a given time. Not sure why the description says that this is only intended for use in long-running services. Anyway, since the above service is an entity auto service, which does not have a service definition, this method cannot be used.
  2. Use the for-update=“true” attribute on the entity-find-one query, which locks the selected record only. This is what the inventory reservation service does. Basically I need to convert the auto store service to an entity find. The code would look something like this:
    ec.entity.find("mantle.order.OrderItem").forUpdate(true) .condition(context).one()

Still, wouldn’t it be better if the framework would automatically lock the record for update or store auto services? Or if it would be too much overhead to do this for each call, maybe provide an option to do it on demand?