Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Insert of JDBC session attribute fails if concurrent request deletes session #1031

Closed
rfajarachmad opened this issue Mar 28, 2018 · 36 comments
Closed
Assignees
Labels
Milestone

Comments

@rfajarachmad
Copy link

Hi everyone,

I'm using spring-session 1.3.2.RELEASE with postgresql, I got this following error on my production environment. When I tried to login into the system, the session was created in the database normally, but somehow this exception occurred, I haven't found how to replicate this. does anyone know how to fix it ?

Oops! Appears that an error has occured. Please refresh the page and try again.. Exception message: PreparedStatementCallback; SQL [INSERT INTO SPRING_SESSION_ATTRIBUTES(SESSION_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) VALUES (?, ?, ?)]; ERROR: insert or update on table "spring_session_attributes" violates foreign key constraint "spring_session_attributes_fk"
  Detail: Key (session_id)=(3483b536-25b7-4206-89b7-2323626ba198) is not present in table "spring_session".; nested exception is org.postgresql.util.PSQLException: ERROR: insert or update on table "spring_session_attributes" violates foreign key constraint "spring_session_attributes_fk"
  Detail: Key (session_id)=(3483b536-25b7-4206-89b7-2323626ba198) is not present in table "spring_session".
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [INSERT INTO SPRING_SESSION_ATTRIBUTES(SESSION_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) VALUES (?, ?, ?)]; ERROR: insert or update on table "spring_session_attributes" violates foreign key constraint "spring_session_attributes_fk"
  Detail: Key (session_id)=(3483b536-25b7-4206-89b7-2323626ba198) is not present in table "spring_session".; nested exception is org.postgresql.util.PSQLException: ERROR: insert or update on table "spring_session_attributes" violates foreign key constraint "spring_session_attributes_fk"
  Detail: Key (session_id)=(3483b536-25b7-4206-89b7-2323626ba198) is not present in table "spring_session".
    at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:243)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:657)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:906)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:967)
    at org.springframework.session.jdbc.JdbcOperationsSessionRepository$2.doInTransactionWithoutResult(JdbcOperationsSessionRepository.java:464)
    at org.springframework.transaction.support.TransactionCallbackWithoutResult.doInTransaction(TransactionCallbackWithoutResult.java:34)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:133)
    at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:418)
    at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:130)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:245)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:217)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:170)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:80)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:615)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:962)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1115)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.postgresql.util.PSQLException: ERROR: insert or update on table "spring_session_attributes" violates foreign key constraint "spring_session_attributes_fk"
  Detail: Key (session_id)=(3483b536-25b7-4206-89b7-2323626ba198) is not present in table "spring_session".
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2182)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1911)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:173)
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:622)
    at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:472)
    at org.postgresql.jdbc.PgStatement.executeUpdate(PgStatement.java:429)
    at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:913)
    at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:906)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:641)
    ... 30 more

The configuration

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>
    <bean class="org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration"/>

    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${driverClassName}" />
        <property name="url" value="${database.url}" />
        <property name="username" value="${database.username}" />
        <property name="password" value="${database.password}" />
    </bean>


    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource"/>
    </bean>

    <bean class="org.springframework.session.web.http.DefaultCookieSerializer">
        <property name="cookieName" value="MYCOOKIE" />
    </bean>
</beans>
@vpavic vpavic self-assigned this Mar 30, 2018
@vpavic
Copy link
Contributor

vpavic commented Mar 30, 2018

Thanks for the report @rfajarachmad.

Can you show us your Spring Session related tables? With psql you can execute these commands to obtain the required information:

> \dS+ spring_session
> \dS+ spring_session_attributes

Also it would be useful to know which sequence of actions has led to this error.

@vpavic vpavic added the status: waiting-for-feedback We need additional information before we can continue label Mar 30, 2018
@cwmccann
Copy link

cwmccann commented Apr 3, 2018

Hi, I've been getting the same errors periodically. Not sure how to reproduce the issue as I found it scanning the logs.

Here is how my tables look:

                                  Table "public.spring_session"
        Column         |          Type          | Modifiers | Storage  | Stats target | Description
-----------------------+------------------------+-----------+----------+--------------+-------------
 primary_id            | character(36)          | not null  | extended |              |
 session_id            | character(36)          | not null  | extended |              |
 creation_time         | bigint                 | not null  | plain    |              |
 last_access_time      | bigint                 | not null  | plain    |              |
 max_inactive_interval | integer                | not null  | plain    |              |
 expiry_time           | bigint                 | not null  | plain    |              |
 principal_name        | character varying(100) |           | extended |              |
Indexes:
    "spring_session_pk" PRIMARY KEY, btree (primary_id)
    "spring_session_ix1" UNIQUE, btree (session_id)
    "spring_session_ix2" btree (expiry_time)
    "spring_session_ix3" btree (principal_name)
Referenced by:
    TABLE "spring_session_attributes" CONSTRAINT "spring_session_attributes_fk" FOREIGN KEY (session_primary_id) REFERENCES spring_session(primary_id) ON DELETE CASCADE

                            Table "public.spring_session_attributes"
       Column       |          Type          | Modifiers | Storage  | Stats target | Description
--------------------+------------------------+-----------+----------+--------------+-------------
 session_primary_id | character(36)          | not null  | extended |              |
 attribute_name     | character varying(200) | not null  | extended |              |
 attribute_bytes    | bytea                  | not null  | extended |              |
Indexes:
    "spring_session_attributes_pk" PRIMARY KEY, btree (session_primary_id, attribute_name)
    "spring_session_attributes_ix1" btree (session_primary_id)
Foreign-key constraints:
    "spring_session_attributes_fk" FOREIGN KEY (session_primary_id) REFERENCES spring_session(primary_id) ON DELETE CASCADE

@cwmccann
Copy link

cwmccann commented Apr 3, 2018

I did add some extra logging to the rest error handler for this particular exception and the session looks intact.

c.m.m.server.config.RestErrorHandler -  Session.getCreationTime() = 1522704779144
c.m.m.server.config.RestErrorHandler -  Session.getLastAccessedTime() = 1522705160150
c.m.m.server.config.RestErrorHandler -  Session.getMaxInactiveInterval() = 1800
c.m.m.server.config.RestErrorHandler -  now = 1522705160490

@vpavic
Copy link
Contributor

vpavic commented Apr 5, 2018

@cwmccann If you are using Spring Session 1.3 then you have configured your database schema incorrectly. The presence of primary_id column in spring_session table indicates that schema scripts for Spring Session 2.0 have been used (see #835).

Make sure you use the correct schema scripts, which are included in the Spring Session JAR itself.

@cwmccann
Copy link

cwmccann commented Apr 5, 2018

I missed that the original issue was with spring session 1.3. I'm using:
org.springframework.session:spring-session-jdbc: -> 2.0.2.RELEASE so it looks like the ddl is at the right level.

@vpavic
Copy link
Contributor

vpavic commented Apr 5, 2018

Thanks for the update @cwmccann. So it appears the same issue, affecting both 1.3 and 2.0.

If someone could provide a sample to reproduce this, that would be great.

@vpavic
Copy link
Contributor

vpavic commented Apr 5, 2018

One additional piece of information that could be useful is how many instances of your application do you run in a cluster? Are the system clocks on all servers in sync?

@cwmccann
Copy link

cwmccann commented Apr 5, 2018

I haven't figured out how to reproduce it yet. In this setup I have 3 services using the jdbc session, 2 on one box and 1 on another. NTP is not setup on them but the time seems to be within a second of each other.

@pminearo
Copy link

I am a little confused with this issue. The original stack trace above is from 1.3.2 because JdbcOperationsSessionRepository.java:464 in 2.0.3 is a "}". But there is mention that it is in both 1.3 and 2.0: "So it appears the same issue, affecting both 1.3 and 2.0.". I am not sure the issue is in 2.0.

We are seeing the same error in our production environment with 1.3.1. However, we are not seeing this error in our testing environment which has 2.0.2. We are releasing out to production in the next week, or so. Maybe something will pop up then; hopefully not. Does anyone have a stack trace for 2.0? Since the code base has changed a fair bit between 1.3 and 2.0; it would be nice to have a newer stack trace.

I was looking through the 2.0 code base and could not see how it could have a primary key issue except in a few situations:

  1. Somehow the parent row in "spring_session" is deleted right before the attribute is inserted.
  2. "ps.setString(1, session.primaryKey);" in JdbcOperationsSessionRepository.java line 502 and 517 has a bogus number, or is NULL(?)
  3. "if (session.isNew()) {" on line 368 is returning false when it should be returning true. Meaning the Session should be new because it doesn't exist anymore? Maybe it was deleted because it timed out or something? However, it is not being flagged as new.

A stack trace from the 2.0.3 code base would be helpful; but I have yet to find one. Also, we are using Spring Security 5.0.4.RELEASE. In production we use: Spring Security 4.2.3.RELEASE and Spring Session 1.3.1.RELEASE. Could there have been something between Spring Security and Spring Session?

@candrews
Copy link
Contributor

I'm pretty sure this is a duplicate of #1070

@vpavic
Copy link
Contributor

vpavic commented May 25, 2018

@candrews I don't think so - #1070 was caused by a regression introduced in 2.0.3.RELEASE while this was reported against earlier releases.

@pminearo
Copy link

pminearo commented Jun 21, 2018

We just got a stack trace for version 2.0.2 I know things have changed in 2.0.3/4, but I think the logic is similar. What it looks like is happening is, somehow the Session is not being flagged as new. This leads to the code trying to update the session, and since updateCount == 0, an insert happens. This causes the Stack Trace.

Since, nothing else is logged out I can not tell what was in the session. However, I cross referenced the time stamp in the stack trace with our access logs. One second before the timestamp on the stack trace there was a logout. The logout returns a 302 (redirect) to send the user back to the login page. One of the frameworks we are using makes a call back to the server before the login page (still trying to figure out why). It seems that the session is not being dropped from the server. It is getting dropped from the DB, but not the server which is Tomcat in our case.

This means the session is not being flagged, or validated, as a new session?? Even though, it is not in the DB. I didn't look upstream in the code to see how the session is found and verified as new. But it does bring up the point that it is possible for a Session to exist in the Server and not in the DB.

  • Should JdbcOperationsSessionRepository validate a Session that is not new before updating?
  • Or is there a better place up stream for a Session to be validated?
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [INSERT INTO SPRING_SESSION_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) VALUES (?, ?, ?)ORA-02291: integrity constraint (ETPBA.SPRING_SESSION_ATTRIBUTES_FK) violated - parent key not found
; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-02291: integrity constraint (ETPBA.SPRING_SESSION_ATTRIBUTES_FK) violated - parent key not found
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:246)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1402)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:620)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:850)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:905)
at org.springframework.session.jdbc.JdbcOperationsSessionRepository$2.doInTransactionWithoutResult(JdbcOperationsSessionRepository.java:445)
at org.springframework.transaction.support.TransactionCallbackWithoutResult.doInTransaction(TransactionCallbackWithoutResult.java:36)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:409)
at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:130)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:228)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:196)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryResponseWrapper.onResponseCommitted(SessionRepositoryFilter.java:184)
at org.springframework.session.web.http.OnCommittedResponseWrapper.doOnResponseCommitted(OnCommittedResponseWrapper.java:227)
at org.springframework.session.web.http.OnCommittedResponseWrapper.access$000(OnCommittedResponseWrapper.java:38)
at org.springframework.session.web.http.OnCommittedResponseWrapper$SaveContextPrintWriter.flush(OnCommittedResponseWrapper.java:249)

@pminearo
Copy link

Did a little more research and we think we figured out what is going on. It's a race condition. Essentially, what is going on is this:

  • The page the user's browser is displaying has an ajax call to refresh the data every x seconds.
  • The user clicks on the logout at the same time the ajax call is being made.
  • The server gets 2 requests and forks 2 threads, 1 for each request, handing each thread a copy of the HttpSession object. The reason the isNew() returns false.
  • The logout request removes the Session information from the DB just before the ajax request tries to update the session tables.
  • The ajax request, having a not new Session, tries to update the spring_session table to which nothing gets updated. Then the request tries to update the spring_session_attribute table to which nothing gets updated. Then it tries to insert the session attributes to which there is no session. This is what throws the Integrity Constraint exception.

I noticed in 2.0.3.RELEASE the code changed to this:

....
List<String> addedAttributeNames = session.delta.entrySet().stream()
	.filter(entry -> entry.getValue() == DeltaValue.ADDED)
	.map(Map.Entry::getKey)
	.collect(Collectors.toList());
insertSessionAttributes(session, addedAttributeNames);
List<String> updatedAttributeNames = session.delta.entrySet().stream()
	.filter(entry -> entry.getValue() == DeltaValue.UPDATED)
	.map(Map.Entry::getKey)
	.collect(Collectors.toList());
updateSessionAttributes(session, updatedAttributeNames);
....

Not sure if the change in logic here will fix this problem. I did not look upstream to see how the DeltaValue is figured out. In a race condition like this, there is still the possibility for the DeltaValue.ADDED to be set even though there is no Session in the DB. So, maybe, right before the insertSessionAttribute(), or in the insertSessionAttribute() method, validate the Session still exists.
If it is done before, the code can skip inserting, updating, and deleting. If done in the method, just the insert gets skipped. The updating and deleting are called, but will be no-ops. Agreeably, this is an edge case; but one that can be easily solved by validating the session before doing anything with the attributes.

@vpavic
Copy link
Contributor

vpavic commented Jun 22, 2018

Thank you for the detailed analysis @pminearo, this is very useful info.

Could you give 2.0.4.RELEASE a try and see if the optimizations introduced in 2.0.3.RELEASE help with you situation? We made those as a part of an effort to optimize write operations, just make sure you try 2.0.4.RELEASE since 2.0.3.RELEASE has a bug related to delta resolution.

@pminearo
Copy link

Will try it next week. Hopefully, we will have some more information within the next couple of weeks.

@pminearo
Copy link

We upgraded our production environment to 2.0.4 and got the stack trace this morning. So, it looks like it is still possible to have a race condition where one thread deletes the session right before the second thread tries to update it. I understand you are trying to optimize write operations, but it seems like it would be a good idea to validate the parent Session ID exists in the DB, which would only be a read, before calling the insertSessionAttributes() method on line 413 in JdbcOperationsSessionRepository. At least that will keep the validation to just the updates and not the inserts of new sessions.

com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor - PreparedStatementCallback; SQL [INSERT INTO SPRING_SESSION_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) VALUES (?, ?, ?)ORA-00001: unique constraint (ETPBA.SPRING_SESSION_ATTRIBUTES_PK) violated
; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (ETPBA.SPRING_SESSION_ATTRIBUTES_PK) violated
org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [INSERT INTO SPRING_SESSION_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) VALUES (?, ?, ?)ORA-00001: unique constraint (ETPBA.SPRING_SESSION_ATTRIBUTES_PK) violated
; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (ETPBA.SPRING_SESSION_ATTRIBUTES_PK) violated
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:242) ~[spring-jdbc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72) ~[spring-jdbc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1402) ~[spring-jdbc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:620) ~[spring-jdbc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:850) ~[spring-jdbc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:905) ~[spring-jdbc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.session.jdbc.JdbcOperationsSessionRepository.insertSessionAttributes(JdbcOperationsSessionRepository.java:515) ~[spring-session-jdbc-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.jdbc.JdbcOperationsSessionRepository.access$300(JdbcOperationsSessionRepository.java:131) ~[spring-session-jdbc-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.jdbc.JdbcOperationsSessionRepository$2.doInTransactionWithoutResult(JdbcOperationsSessionRepository.java:413) ~[spring-session-jdbc-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.transaction.support.TransactionCallbackWithoutResult.doInTransaction(TransactionCallbackWithoutResult.java:36) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:393) ~[spring-session-jdbc-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:131) ~[spring-session-jdbc-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:234) ~[spring-session-core-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:197) ~[spring-session-core-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryResponseWrapper.onResponseCommitted(SessionRepositoryFilter.java:185) ~[spring-session-core-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.web.http.OnCommittedResponseWrapper.doOnResponseCommitted(OnCommittedResponseWrapper.java:227) ~[spring-session-core-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.web.http.OnCommittedResponseWrapper.access$000(OnCommittedResponseWrapper.java:38) ~[spring-session-core-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.session.web.http.OnCommittedResponseWrapper$SaveContextPrintWriter.close(OnCommittedResponseWrapper.java:255) ~[spring-session-core-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:417) ~[catalina.jar:8.0.52]
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:318) ~[catalina.jar:8.0.52]
at org.apache.tiles.request.servlet.ServletRequest.forward(ServletRequest.java:265) ~[tiles-request-servlet-1.0.7.jar:1.0.7]
at org.apache.tiles.request.servlet.ServletRequest.doForward(ServletRequest.java:228) ~[tiles-request-servlet-1.0.7.jar:1.0.7]
at org.apache.tiles.request.AbstractClientRequest.dispatch(AbstractClientRequest.java:57) ~[tiles-request-api-1.0.7.jar:1.0.7]
at org.apache.tiles.request.render.DispatchRenderer.render(DispatchRenderer.java:47) ~[tiles-request-api-1.0.7.jar:1.0.7]
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:259) ~[tiles-core-3.0.8.jar:3.0.8]
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:397) ~[tiles-core-3.0.8.jar:3.0.8]
at org.apache.tiles.impl.BasicTilesContainer.render(BasicTilesContainer.java:238) ~[tiles-core-3.0.8.jar:3.0.8]
at org.apache.tiles.TilesContainerWrapper.render(TilesContainerWrapper.java:103) ~[tiles-api-3.0.8.jar:3.0.8]
at org.apache.tiles.impl.mgmt.CachingTilesContainer.render(CachingTilesContainer.java:126) ~[tiles-core-3.0.8.jar:3.0.8]
at org.apache.struts2.views.tiles.TilesResult.doExecute(TilesResult.java:158) ~[struts2-tiles-plugin-2.5.14.1.jar:2.5.14.1]
at org.apache.struts2.result.StrutsResultSupport.execute(StrutsResultSupport.java:204) ~[struts2-core-2.5.14.1.jar:2.5.14.1]

@LouisVN
Copy link

LouisVN commented Jul 10, 2018

We are seeing this error hundreds of time per day on production. We have 5 application nodes running and thousands of concurrent users.
We also upgraded from version 2.0.2.RELEASE to 2.0.4.RELEASE but did not see much improvement unfortunately.

Would you have any estimation on the time to release a fix ?

Thanks in advance !

@vpavic vpavic added this to the 2.0.5 milestone Jul 10, 2018
@vpavic
Copy link
Contributor

vpavic commented Jul 10, 2018

I've just assigned this to 2.0.5.RELEASE and will try to get a closer look this or next week.

@LouisVN Can you confirm the scenario in which you're encountering this error is similar to one described by @pminearo, i.e. concurrent requests where one invalidates/deletes the session while other attempts to update it?

@LouisVN
Copy link

LouisVN commented Jul 13, 2018

Good to hear. Thanks for that @vpavic !

I ran some experimentation and crossed-checked with the production environment.
Actually my previous statement about 2.0.4.RELEASE was wrong. It did improve the average response time per request but also reduced the number of exceptions raised compared to 2.0.2.RELEASE.

So I do believe that the scenario mentioned by @pminearo is the remaining point to be fixed.

@vpavic
Copy link
Contributor

vpavic commented Jul 13, 2018

I have been trying to reproduce this locally, but have so far been unable to do so. I've tried using H2, PostgreSQL and MariaDB.

My test scenario is basically something like this:

  • login i.e. initiate session
  • access slow endpoint (something that blocks a thread for a few seconds on the server) in one browser tab
  • logout in other browser tab

@pminearo & @LouisVN, could you provide as much info as possible about your database, which vendor do you use (@pminearo from stacktraces you posted I assume it's Oracle in your case), which version, as well as provide DDL dumps of the Spring Session tables.

Additionally, how many instances of your application do you run in a cluster and do the concurrent requests involved end up being processed on the same or different servers?

@vpavic vpavic added the status: waiting-for-feedback We need additional information before we can continue label Jul 13, 2018
@LouisVN
Copy link

LouisVN commented Jul 16, 2018

We have the following :

  • Replicated DB cluster with MySQL v5.7.22.
  • 5 application nodes/instances.
  • Concurrent requests should reach the same server (we strive to ensure session stickiness) but there is no guarantee.

The DDL is :

CREATE DATABASE `sessions` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

CREATE TABLE `SPRING_SESSION` (
  `PRIMARY_ID` char(36) NOT NULL,
  `SESSION_ID` char(36) NOT NULL,
  `CREATION_TIME` bigint(20) NOT NULL,
  `LAST_ACCESS_TIME` bigint(20) NOT NULL,
  `MAX_INACTIVE_INTERVAL` int(11) NOT NULL,
  `EXPIRY_TIME` bigint(20) NOT NULL,
  `PRINCIPAL_NAME` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`PRIMARY_ID`),
  UNIQUE KEY `SPRING_SESSION_IX1` (`SESSION_ID`),
  KEY `SPRING_SESSION_IX2` (`EXPIRY_TIME`),
  KEY `SPRING_SESSION_IX3` (`PRINCIPAL_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `SPRING_SESSION_ATTRIBUTES` (
  `SESSION_PRIMARY_ID` char(36) NOT NULL,
  `ATTRIBUTE_NAME` varchar(200) NOT NULL,
  `ATTRIBUTE_BYTES` mediumblob NOT NULL,
  PRIMARY KEY (`SESSION_PRIMARY_ID`,`ATTRIBUTE_NAME`),
  KEY `SPRING_SESSION_ATTRIBUTES_IX1` (`SESSION_PRIMARY_ID`),
  CONSTRAINT `SPRING_SESSION_ATTRIBUTES_FK` FOREIGN KEY (`SESSION_PRIMARY_ID`) REFERENCES `SPRING_SESSION` (`PRIMARY_ID`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

It does not seem to happen much since we upgraded to the last version. It now seems like an edge use case.

@pminearo
Copy link

Interesting @LouisVN saw less exceptions when upgrading, when we saw an increase in exceptions when we upgraded to 2.0.4.RELEASE.

  • Oracle 10c
  • One application per client. So, concurrent requests will reach the same server every time. Each app has pages with ajax calls that refresh the data on the page every 15 seconds.

Reproducing the problem is difficult because it is a race condition. You have to time the requests in such a way that they both get the session and have it flagged as not new and then have the first session remove the entries from spring_session and spring_session_attribute before the second request tries to insert data in spring_session_attribute.

I understand you are trying to increase performance and reduce the number of writes. But a quick check to ensure the Session ID exists before writing to the Session Attribute table should not be a significant decrease in performance. Especially, since it is a read. You just need to confirm the session still exists before writing.

The other option is to catch any exception when trying to write to the spring_session_attribute table and log it as info, or warn, or just return assuming the user's session is over with anyways.

Our problem is the exception is getting kicked out to the container which sends the on-call person a CRITICAL error text when this isn't a critical error. There is no way for us to control the logging level of the exception.

CREATE TABLE "SPRING_SESSION"  (	
	"PRIMARY_ID" CHAR(36 BYTE) NOT NULL ENABLE, 
	"SESSION_ID" CHAR(36 BYTE) NOT NULL ENABLE, 
	"CREATION_TIME" NUMBER(19,0) NOT NULL ENABLE, 
	"LAST_ACCESS_TIME" NUMBER(19,0) NOT NULL ENABLE, 
	"MAX_INACTIVE_INTERVAL" NUMBER(10,0) NOT NULL ENABLE, 
	"EXPIRY_TIME" NUMBER(19,0) NOT NULL ENABLE, 
	"PRINCIPAL_NAME" VARCHAR2(100 CHAR), 
	CONSTRAINT "SPRING_SESSION_PK" PRIMARY KEY ("PRIMARY_ID") 
)

CREATE UNIQUE INDEX "SPRING_SESSION_IX1" ON "SPRING_SESSION" ("SESSION_ID");
CREATE INDEX "SPRING_SESSION_IX2" ON "SPRING_SESSION" ("EXPIRY_TIME");
CREATE INDEX "SPRING_SESSION_IX3" ON "SPRING_SESSION" ("PRINCIPAL_NAME");

CREATE TABLE "SPRING_SESSION_ATTRIBUTES" (	
	"SESSION_PRIMARY_ID" CHAR(36 BYTE) NOT NULL ENABLE, 
	"ATTRIBUTE_NAME" VARCHAR2(200 CHAR) NOT NULL ENABLE, 
	"ATTRIBUTE_BYTES" BLOB NOT NULL ENABLE, 
	CONSTRAINT "SPRING_SESSION_ATTRIBUTES_PK" PRIMARY KEY ("SESSION_PRIMARY_ID", "ATTRIBUTE_NAME")
	CONSTRAINT "SPRING_SESSION_ATTRIBUTES_FK" FOREIGN KEY ("SESSION_PRIMARY_ID") REFERENCES "SPRING_SESSION" ("PRIMARY_ID") ON DELETE CASCADE ENABLE
);

CREATE INDEX "SPRING_SESSION_ATTRIBUTES_IX1" ON "SPRING_SESSION_ATTRIBUTES" ("SESSION_PRIMARY_ID");

@candrews
Copy link
Contributor

Using REPLACE INTO (or equivalent functionality, if available) would also work. Then only one query would continue to be necessary (instead of checking then inserting, which is two queries).

@vpavic vpavic modified the milestones: 2.0.5, 2.0.6 Jul 26, 2018
@vpavic
Copy link
Contributor

vpavic commented Aug 9, 2018

I've finally managed to dedicate some more time to this and reproduce the problem locally.

I believe the optimal way to address this would be change SQL used to insert session attribute to be a conditional statement. This wouldn't require issuing additional SELECT statement beforehand.

Specifically, this means that instead of:

INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
VALUES (?, ?, ?)

we'd use:

INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
SELECT PRIMARY_ID, ?, ?
FROM %TABLE_NAME%
WHERE SESSION_ID = ?

@pminearo and @LouisVN, do you have any thoughts on this proposal?

vpavic added a commit to vpavic/spring-session that referenced this issue Aug 9, 2018
At present, the insert of new attributes in JdbcOperationsSessionRepository is done unconditionally. This can cause data integrity violation errors with concurrent requests, where one request attempts to add new session attribute while the other, concurrent request, deletes the session.

This commit addresses the described scenario by executing insert of new attributes conditionally on presence of parent record.

Closes spring-projectsgh-1031
@vpavic
Copy link
Contributor

vpavic commented Aug 9, 2018

I've pushed the proposed solution to this branch in my fork. The commit also includes a test that reproduces the problem.

@vpavic vpavic changed the title Key (session_id)=(xxx) is not present in table "spring_session" Insert of JDBC session attribute fails if concurrent request deletes session Aug 9, 2018
@vpavic vpavic modified the milestones: 2.0.6, 2.1.0.M2 Aug 9, 2018
@vpavic
Copy link
Contributor

vpavic commented Aug 10, 2018

One additional detail I'm considering is to also add some logging when affected record count returned by
JdbcOperations doesn't match the expected (i.e. affected record count is zero). This should provide an indication that save was likely invoked on a removed session.

@vpavic vpavic closed this as completed in 87c2e53 Aug 13, 2018
@vpavic
Copy link
Contributor

vpavic commented Aug 13, 2018

This is now fixed in master via 87c2e53, and backported to 2.0.x (see #1151) and 1.3.x (see #1153).

@LouisVN
Copy link

LouisVN commented Aug 13, 2018

Thanks @vpavic, this looks good at a quick glance.
Sorry for the delay answering.

@vpavic
Copy link
Contributor

vpavic commented Aug 13, 2018

Thanks for the feedback @LouisVN. Snapshot are available from https://repo.spring.io/libs-snapshot/ so if you're able to give it a spin, we'd greatly appreciate it.

vpavic added a commit that referenced this issue Sep 25, 2018
JdbcOperationsSessionRepository recently introduced validation when inserting new session attributes in order to prevent data integrity violations in highly concurrent environments. This is done by using INSERT INTO ... SELECT statement to verify existence of session record in parent table. Such arrangement causes problems with Oracle if inserted attribute is of size 4 kb or more.

This commit enhances JdbcHttpSessionConfiguration to detect Oracle database is used, and set createTemporaryLob option on default LobHandler to true.

Resolves: #1203
See also: #1031
@blacelle
Copy link

blacelle commented Jan 7, 2019

Hello We observed this behavior with spring-session 2.1.2 Which information can we provide to help investigation ?

@vpavic
Copy link
Contributor

vpavic commented Jan 7, 2019

@blacelle Are you sure your comment references the right Spring Session version? I'm asking because JDBC session store support was added in Spring Session 1.2.0.RELEASE.

@blacelle
Copy link

blacelle commented Jan 7, 2019

@vpavic You are right. I meant 2.1.2.RELEASE (the original comment has been edited)

@vpavic
Copy link
Contributor

vpavic commented Jan 7, 2019

Thanks for clarifying @blacelle. Are you sure you're encountering the problem reported here and not #1213? If it's not #1213 could you please open a new issue, together with a sample app to reproduce the problem?

@johneweak
Copy link

Hello, I also encounter the same issue reported here using spring-session 2.1.4, springboot 2.1.3.RELEASE. I cannot find the new issue opened by @blacelle

The DDL script used is: sql

nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (DB2.SPRING_SESSION_ATTRIBUTES_PK) violated

In our setup, we have multiple springboot applications being accessed by a client simultaneously. Each application point to the same db (spring session tables) to validate session.

@blacelle
Copy link

blacelle commented Aug 2, 2019

@Exakt I felt my issue was the same as #1213

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants