0
votes

I've read the SO suggested questions related to this one and checked if I have the same issue as version, concurrent modification, etc but I think mine is slightly different.

Stack:

  • JavaEE
  • PostgreSQL
  • JSF2.2
  • Primefaces

I'm saving an entity Endpoint and it works on the first call, but saving the same entity again throws the error in the title.

Here's the entity class: https://github.com/meveo-org/meveo/blob/develop/meveo-admin/ejbs/src/main/java/org/meveo/service/technicalservice/endpoint/EndpointService.java

public E update(E entity) throws BusinessException {
    preUpdate(entity);
    try {
        entity = getEntityManager().merge(entity);
    } catch(Exception e) {
        if(e instanceof UndeclaredThrowableException) {
            throw new BusinessException(e.getCause().getCause());
        } else {
            throw new BusinessException(e);
        }
    }       
    postUpdate(entity);
    return entity;
}

public void updateNoMerge(E entity) throws BusinessException {
    preUpdate(entity);     
    postUpdate(entity);
}

And here is the service that saves the entity: https://github.com/meveo-org/meveo/blob/develop/meveo-model/src/main/java/org/meveo/model/technicalservice/endpoint/Endpoint.java

@Entity
@Table(name = "service_endpoint")
@GenericGenerator(name = "ID_GENERATOR", strategy = "increment")
@NoIntersectionBetween(
firstCollection = "pathParameters.endpointParameter.parameter",
secondCollection = "parametersMapping.endpointParameter.parameter"
)
@NamedQueries({
@NamedQuery(name = "findByParameterName", query = "SELECT e FROM Endpoint e " +
"INNER JOIN e.service as service " +
"LEFT JOIN e.pathParameters as pathParameter " +
"LEFT JOIN e.parametersMapping as parameterMapping " +
"WHERE service.code = :serviceCode " +
"AND (pathParameter.endpointParameter.parameter = :propertyName OR parameterMapping.endpointParameter.parameter = :propertyName)"),
@NamedQuery(name = "Endpoint.deleteByService", query = "DELETE from Endpoint e WHERE e.service.id=:serviceId")})
@ImportOrder(5)
@ExportIdentifier({ "code" })
@ModuleItem("Endpoint")
@ModuleItemOrder(80)
@ObservableEntity
public class Endpoint extends BusinessEntity {

private static final long serialVersionUID = 6561905332917884613L;

@ElementCollection(fetch = FetchType.LAZY)
@Fetch(value = FetchMode.SUBSELECT)
@CollectionTable(name = "service_endpoint_roles", joinColumns = @JoinColumn(name = "endpoint_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();

/**
* Technical service associated to the endpoint
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "service_id", updatable = false, nullable = false)
private Function service;

/**
* Whether the execution of the service will be syncrhonous.
* If asynchronous, and id of execution will be returned to the user.
*/
@Type(type = "numeric_boolean")
@Column(name = "synchronous", nullable = false)
private boolean synchronous;

/**
* Method used to access the endpoint.
* Conditionates the input format of the endpoint.
*/
@Enumerated(EnumType.STRING)
@Column(name = "method", nullable = false)
private EndpointHttpMethod method;

/**
* Parameters that will be exposed in the endpoint path
*/
@OneToMany(mappedBy = "endpointParameter.endpoint", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@OrderColumn(name = "position")
private List<EndpointPathParameter> pathParameters = new ArrayList<>();

/**
* Mapping of the parameters that are not defined as path parameters
*/
@OneToMany(mappedBy = "endpointParameter.endpoint", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<TSParameterMapping> parametersMapping = new ArrayList<>();

/**
* JSONata query used to transform the result
*/
@Column(name = "jsonata_transformer")
private String jsonataTransformer;

/**
* Context variable to be returned by the endpoint
*/
@Column(name = "returned_variable_name")
private String returnedVariableName;

/**
* Context variable to be returned by the endpoint
*/
@Type(type = "numeric_boolean")
@Column(name = "serialize_result", nullable = false)
private boolean serializeResult;

/**
* Content type of the response
*/
@Column(name = "content_type")
private String contentType;

The persistence.xml file https://github.com/meveo-org/meveo/blob/master/meveo-admin/web/src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

    <persistence-unit name="MeveoAdmin">
        <jta-data-source>java:jboss/datasources/MeveoAdminDatasource</jta-data-source>
        <jar-file>lib/meveo-model-${project.version}.jar</jar-file>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <properties>
            <property name="hibernate.archive.autodetection" value="class" />
            <property name="hibernate.hbm2ddl.auto" value="validate" /> <!-- DB structure is managed by liquibase, not hibernate -->
            <property name="hibernate.show_sql" value="true" />
            <property name="format_sql" value="true"/>
            <property name="use_sql_comments" value="false"/>
            <property name="hibernate.connection.harSet" value="utf-8"/>
            <property name="hibernate.connection.useUnicode" value="true"/>
            <property name="hibernate.connection.characterEncoding" value="UTF-8" />
            <!-- <property name="hibernate.default_schema" value="public" /> Disable for Mysql/mariaDB instalation -->
            <property name="hibernate.cache.use_second_level_cache" value="true" />
            <property name="hibernate.cache.use_query_cache" value="true" />
            <property name="hibernate.cache.use_minimal_puts" value="true" />
            <property name="hibernate.cache.default_cache_concurrency_strategy" value="transactional" />
            <property name="hibernate.generate_statistics" value="false" />
            <property name="hibernate.discriminator.ignore_explicit_for_joined" value="true" />
            <property name="hibernate.ejb.event.flush" value="org.meveo.jpa.event.FlushEventListener" /> <!-- Needed for ES -->
            <property name="hibernate.jpa.compliance.global_id_generators" value="false"/>
            <property name="hibernate.c3p0.min_size" value="5"></property>
            <property name="hibernate.c3p0.max_size" value="20"></property>
            <property name="hibernate.c3p0.acquire_increment" value="5"></property>
            <property name="hibernate.c3p0.timeout" value="1800"></property>
<!--            <property name="hibernate.persister.resolver" value="org.hibernate.util.CustomPersisterClassResolver"></property> -->
            <property name="hibernate.transaction.factory_class" value="org.hibernate.transaction.JTATransactionFactory"/>
            <property name="hibernate.connection.isolation" value="4"></property>
        </properties>
    </persistence-unit>

In EndpointService.update, I tried experimenting with:

  1. super.update - calls em.merge
  2. super.updateNoMerge - basically does nothing with regards to the entity, it just calls some pre and posts update triggers.

In the second update I got these logs for both methods above:

19:31:31,022 ERROR [org.jboss.as.ejb3.invocation] (default task-1) WFLYEJB0034: EJB Invocation failed on component EndpointService for method public long org.meveo.service.base.PersistenceService.count(org.meveo.admin.util.pagination.PaginationConfiguration): javax.ejb.EJBException: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.meveo.model.technicalservice.endpoint.Endpoint#1] ->Which is triggered when count query is executed. I think at this point hibernate decides to flush the save transactions.

more logs here...

Caused by: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.meveo.model.technicalservice.endpoint.Endpoint#1]

After the error, save the entity again and it will work. It's this cycle:

1,0,PAGE_RELOAD,1,0

Where 1 is successful and 0 fails.

I already checked the entity relationships as well as child entities but I couldn't figure out the problem.

Any idea?

Here's the full error log: https://www.dropbox.com/s/a0r12rf1vf6rshe/optimisticlockexception.txt

1
Could you please post your code instead of linking to external resources? Also, is there a chance the two updates are executed simultaneously? I wouldn't immediately jump to the conclusion that there is a problem, 99% of the time an OptimisticLockException protects you from race conditionscrizzis
I have added the codes. I don't think so as it's a synchronous event trigger by a JSF button. And even after waiting for a while, I got the same error on second try.czetsuya

1 Answers

0
votes

Cause of the problem: Event Observer defined in the same service.

public void onEndpointUpdate(@Observes E entity) {
    // call another service that manipulates the entity here.
}

Solution: Refactor the event listener to another stateless class to avoid the stale exception.