Skip to content

Saving JDBC Session Attributes is not thread-safe #1216

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

Closed
bonhamcm opened this issue Oct 5, 2018 · 5 comments
Closed

Saving JDBC Session Attributes is not thread-safe #1216

bonhamcm opened this issue Oct 5, 2018 · 5 comments
Assignees
Labels
status: duplicate A duplicate of another issue

Comments

@bonhamcm
Copy link

bonhamcm commented Oct 5, 2018

Spring Framework 5.0.9.RELEASE
Spring Boot 2.0.5.RELEASE
Spring Session 2.0.6.RELEASE
PostgreSQL 9.3.20
PostgreSQL JDBC Driver 42.2.5

I'm experiencing the same error described in #1213 but I am using Spring Session 2.0.6.RELEASE which has the fix for #1031 / #1151:

org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [INSERT INTO SPRING_SESSION_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) SELECT PRIMARY_ID, ?, ? FROM SPRING_SESSION WHERE SESSION_ID = ?ERROR: duplicate key value violates unique constraint "spring_session_attributes_pk"
  Detail: Key (session_primary_id, attribute_name)=(b020c2e9-5470-4901-b89b-0531b61cf108, survey) already exists.; nested exception is org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "spring_session_attributes_pk"
  Detail: Key (session_primary_id, attribute_name)=(b020c2e9-5470-4901-b89b-0531b61cf108, survey) already exists.
        at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:242)
        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.insertSessionAttributes(JdbcOperationsSessionRepository.java:521)
        at org.springframework.session.jdbc.JdbcOperationsSessionRepository.access$300(JdbcOperationsSessionRepository.java:131)
        at org.springframework.session.jdbc.JdbcOperationsSessionRepository$2.doInTransactionWithoutResult(JdbcOperationsSessionRepository.java:416)
        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:395)
        at org.springframework.session.jdbc.JdbcOperationsSessionRepository.save(JdbcOperationsSessionRepository.java:131)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:234)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:197)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryResponseWrapper.onResponseCommitted(SessionRepositoryFilter.java:185)
        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)
        at org.springframework.security.web.util.OnCommittedResponseWrapper$SaveContextPrintWriter.flush(OnCommittedResponseWrapper.java:269)
        at org.apache.jasper.runtime.JspWriterImpl.flush(JspWriterImpl.java:172)
        ...
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "spring_session_attributes_pk"
  Detail: Key (session_primary_id, attribute_name)=(b020c2e9-5470-4901-b89b-0531b61cf108, survey) already exists.
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2440)
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2183)
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:308)
        at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:441)
        at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:365)
        at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:143)
        at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:120)
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
        at org.springframework.jdbc.core.JdbcTemplate.lambda$update$0(JdbcTemplate.java:855)
        at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:605)
        ... 141 common frames omitted

It appears that the JdbcOperationsSessionRepository$JdbcSession.delta HashMap which is used to determine if the session attributes should be inserted or updated is not thread-safe, thereby causing this issue. Using a ConcurrentHashMap could be a simple fix for this issue.

@rwinch
Copy link
Member

rwinch commented Oct 6, 2018

Thanks for the report!

NOTE: I don't think that ConcurrentHashMap helps since a new instance of the session is used per thread.

@vpavic
Copy link
Contributor

vpavic commented Oct 6, 2018

Thanks for the report @bonhamcm. Actually this isn't a thread safety issue, as the same scenario can easily occur in a clustered setup, where concurrent requests are being load-balanced to different application instances.

I'll try and reproduce this locally.

@bonhamcm
Copy link
Author

I've found that this issue occurs when a user clicks one link and the accidentally clicks it again within one second of the first one.

@vpavic
Copy link
Contributor

vpavic commented Oct 30, 2018

Thanks for the follow-up @bonhamcm - if that's the case I believe it should be fairly easy to prevent such scenario by adding some JavaScript code that disables (either permanently or temporarily) the link/button after it was clicked.

That being said, I'm going to close this one as duplicate of #1213 since that one was opened first (I mistakenly closed it as duplicate, but reopened it shortly after). Please track #1213 for future progress on this, and feel free to add additional feedback there.

@vpavic vpavic closed this as completed Oct 30, 2018
@vpavic
Copy link
Contributor

vpavic commented Oct 30, 2018

Duplicate of #1213

@vpavic vpavic marked this as a duplicate of #1213 Oct 30, 2018
@vpavic vpavic added status: duplicate A duplicate of another issue and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue
Projects
None yet
Development

No branches or pull requests

3 participants