-
Notifications
You must be signed in to change notification settings - Fork 227
Oracle sessionless transaction implementation #3887
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
Open
msupic
wants to merge
39
commits into
5.1.x
Choose a base branch
from
sessionless-transaction
base: 5.1.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 32 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
3e4d786
Prepared test for sessionless transaction implementation
msupic 7e2af32
Merge branch '5.0.x' of github.com:micronaut-projects/micronaut-data …
msupic 09e94ba
Added OracleDataSourceTransactionManager
msupic e11e962
Oracle sessionless transaction impl - wip
msupic e274938
Oracle sessionless transaction impl - wip
msupic 5886098
Oracle sessionless transaction impl - wip
msupic a8d7af9
Oracle sessionless transaction impl - wip
msupic a8b5c6c
Oracle sessionless transaction impl - wip
msupic 485f332
Oracle sessionless transaction impl - wip
msupic 8a7c815
Oracle sessionless transaction impl - wip
msupic c0a3c6a
Oracle sessionless transaction impl - wip
msupic 666e7d4
Oracle sessionless transaction impl - wip
msupic 2a7b77a
Oracle sessionless transaction impl - wip
msupic c4090f1
Oracle sessionless transaction impl - wip
msupic 8fb4fdd
Oracle sessionless transaction impl - wip
msupic 2dbbb8c
Oracle sessionless transaction impl - wip
msupic e7b4f28
Merge branch '5.0.x' of github.com:micronaut-projects/micronaut-data …
msupic 997bf57
Oracle sessionless transaction impl - wip
msupic 4ecb82f
Oracle sessionless transaction impl - wip
msupic 95f4119
Oracle sessionless transaction impl - wip
msupic 907e76b
Oracle sessionless transaction impl - Test Build
msupic 59bdc91
Oracle sessionless transaction impl - Rolled back temp change
msupic a8e5f10
Oracle sessionless transaction impl - added docs
msupic 3ebd342
Merge branch '5.0.x' of github.com:micronaut-projects/micronaut-data …
msupic 1f8b782
Oracle sessionless transaction impl - modified docs
msupic 656e9c8
Oracle sessionless transaction impl - added more tests
msupic 2d30be5
Merge branch '5.0.x' of github.com:micronaut-projects/micronaut-data …
msupic fb17bc6
Oracle sessionless transaction impl - reduced visibility of classes, …
msupic c140360
Renamed jdbc-sessionless-transaction-booking-java module
msupic 92b62bc
Addressed code review comments
msupic 55f47a0
Addressed code review comments
msupic 9a7fe9c
Addressed code review comments
msupic d391144
Fixed native image issue
msupic c8e2359
Addressed sonarqube reported issues
msupic 477c74f
Merge branch '5.1.x' of github.com:micronaut-projects/micronaut-data …
msupic ff80d52
Removed unnecessary changes
msupic c72c6df
Merge branch '5.1.x' of github.com:micronaut-projects/micronaut-data …
msupic 0d28a1e
Merge branch '5.1.x' of github.com:micronaut-projects/micronaut-data …
msupic fa1b6f7
Replaced the Oracle-specific SUSPEND and REQUIRES_SUSPENDED transaction
msupic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
305 changes: 305 additions & 0 deletions
305
...cronaut/data/jdbc/oraclexe/sessionless/OracleSessionlessTransactionPropagationSpec.groovy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,305 @@ | ||
| /* | ||
| * Copyright 2017-2020 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.data.jdbc.oraclexe.sessionless | ||
|
|
||
| import io.micronaut.context.annotation.Property | ||
| import io.micronaut.context.annotation.Requires | ||
| import io.micronaut.data.annotation.GeneratedValue | ||
| import io.micronaut.data.annotation.Id | ||
| import io.micronaut.data.annotation.MappedEntity | ||
| import io.micronaut.data.annotation.Query | ||
| import io.micronaut.data.jdbc.annotation.JdbcRepository | ||
| import io.micronaut.data.jdbc.oraclexe.OracleTestPropertyProvider | ||
| import io.micronaut.data.model.query.builder.sql.Dialect | ||
| import io.micronaut.data.repository.CrudRepository | ||
| import io.micronaut.http.HttpRequest | ||
| import io.micronaut.http.HttpResponse | ||
| import io.micronaut.http.HttpStatus | ||
| import io.micronaut.http.annotation.Controller | ||
| import io.micronaut.http.annotation.Post | ||
| import io.micronaut.http.annotation.Status | ||
| import io.micronaut.http.client.HttpClient | ||
| import io.micronaut.http.client.annotation.Client | ||
| import io.micronaut.http.client.exceptions.HttpClientResponseException | ||
| import io.micronaut.scheduling.TaskExecutors | ||
| import io.micronaut.scheduling.annotation.ExecuteOn | ||
| import io.micronaut.test.extensions.spock.annotation.MicronautTest | ||
| import io.micronaut.transaction.TransactionDefinition | ||
| import io.micronaut.transaction.annotation.Transactional | ||
| import io.micronaut.transaction.jdbc.oracle.OracleSessionlessTransactionHttpConfiguration | ||
| import io.micronaut.transaction.jdbc.oracle.OracleSessionlessTransactionPropagationOperations | ||
| import jakarta.inject.Inject | ||
| import jakarta.inject.Singleton | ||
| import spock.lang.Specification | ||
|
|
||
| @Property(name = "spec.name", value = OracleSessionlessTransactionPropagationSpec.SPEC_NAME) | ||
| @Property(name = "micronaut.data.oracle.sessionless.http.propagation-enabled", value = "true") | ||
| @Property(name = "micronaut.http.client.read-timeout", value = "600s") | ||
| @MicronautTest(transactional = false) | ||
| class OracleSessionlessTransactionPropagationSpec extends Specification implements OracleTestPropertyProvider { | ||
|
|
||
| static final String SPEC_NAME = "OracleSessionlessTransactionPropagationSpec" | ||
| private static final String SESSIONLESS_TRANSACTION_HEADER = OracleSessionlessTransactionHttpConfiguration.DEFAULT_HEADER_NAME | ||
|
|
||
| @Inject | ||
| ExpenseReportService expenseReportService | ||
|
|
||
| @Inject | ||
| ExpenseReportRepository expenseReportRepository | ||
|
|
||
| @Inject | ||
| OracleSessionlessTransactionPropagationOperations transactionPropagationOperations | ||
|
|
||
| @Inject | ||
| @Client("/") | ||
| HttpClient client | ||
|
|
||
| @Override | ||
| List<String> packages() { | ||
| [getClass().package.name] | ||
| } | ||
|
|
||
| void setup() { | ||
| expenseReportRepository.deleteAll() | ||
| } | ||
|
|
||
| void "http propagation commits a suspended expense report approval"() { | ||
| when: | ||
| SubmittedExpenseReport report = submitViaHttp("employee-http-1", "travel", 125.75) | ||
|
|
||
| then: | ||
| expenseReportRepository.findById(report.id()).isEmpty() | ||
|
|
||
| when: | ||
| HttpRequest<String> approveRequest = HttpRequest.POST("/expense-reports/approve/" + report.id(), "") | ||
| .header(SESSIONLESS_TRANSACTION_HEADER, report.transactionId()) | ||
| HttpResponse<Void> approveResponse = client.toBlocking().exchange(approveRequest, Void) | ||
|
|
||
| then: | ||
| approveResponse.status == HttpStatus.NO_CONTENT | ||
| !approveResponse.headers.contains(SESSIONLESS_TRANSACTION_HEADER) | ||
| expenseReportRepository.findById(report.id()).orElseThrow().status == "APPROVED" | ||
| } | ||
|
|
||
| void "http propagation rolls back a resumed expense report approval"() { | ||
| when: | ||
| SubmittedExpenseReport report = submitViaHttp("employee-http-2", "meals", 43.20) | ||
|
|
||
| then: | ||
| expenseReportRepository.findById(report.id()).isEmpty() | ||
|
|
||
| when: | ||
| HttpRequest<String> rejectRequest = HttpRequest.POST("/expense-reports/reject/" + report.id(), "") | ||
| .header(SESSIONLESS_TRANSACTION_HEADER, report.transactionId()) | ||
| client.toBlocking().exchange(rejectRequest, Void) | ||
|
|
||
| then: | ||
| HttpClientResponseException e = thrown() | ||
| e.status == HttpStatus.INTERNAL_SERVER_ERROR | ||
| !e.response.headers.contains(SESSIONLESS_TRANSACTION_HEADER) | ||
| expenseReportRepository.findById(report.id()).isEmpty() | ||
| } | ||
|
|
||
| void "programmatic propagation commits a suspended expense report approval"() { | ||
| when: | ||
| SubmittedExpenseReport report = suspendReport("employee-prog-1", "lodging", 318.40) | ||
|
|
||
| then: | ||
| expenseReportRepository.findById(report.id()).isEmpty() | ||
|
|
||
| when: | ||
| transactionPropagationOperations.withPropagation(report.transactionId(), { | ||
| expenseReportService.approveReport(report.id()) | ||
| null | ||
| }) | ||
|
|
||
| then: | ||
| expenseReportRepository.findById(report.id()).orElseThrow().status == "APPROVED" | ||
| } | ||
|
|
||
| void "programmatic propagation rolls back a resumed expense report approval"() { | ||
| given: | ||
| SubmittedExpenseReport report = suspendReport("employee-prog-2", "software", 799.99) | ||
| assert expenseReportRepository.findById(report.id()).isEmpty() | ||
|
|
||
| when: | ||
| transactionPropagationOperations.withPropagation(report.transactionId(), { | ||
| expenseReportService.rejectReport(report.id()) | ||
| null | ||
| }) | ||
|
|
||
| then: | ||
| ExpenseRejectedException e = thrown() | ||
| e.message == "Expense report failed policy check" | ||
| expenseReportRepository.findById(report.id()).isEmpty() | ||
| } | ||
|
|
||
| void "nested programmatic propagation restores the outer expense report approval"() { | ||
| when: | ||
| List<Long> reportIds = Objects.requireNonNull(transactionPropagationOperations.withPropagation({ | ||
| Long outerReportId = expenseReportService.submitReport("employee-nested-outer", "conference", 640.00) | ||
| String outerTransactionId = transactionPropagationOperations.currentTransactionId().orElseThrow() | ||
| assert expenseReportRepository.findById(outerReportId).isEmpty() | ||
|
|
||
| SubmittedExpenseReport innerReport = Objects.requireNonNull(transactionPropagationOperations.withPropagation({ | ||
| Long innerReportId = expenseReportService.submitReport("employee-nested-inner", "training", 215.25) | ||
| String innerTransactionId = transactionPropagationOperations.currentTransactionId().orElseThrow() | ||
| assert innerTransactionId != outerTransactionId | ||
| assert expenseReportRepository.findById(innerReportId).isEmpty() | ||
|
|
||
| expenseReportService.approveReport(innerReportId) | ||
| assert transactionPropagationOperations.currentTransactionId().isEmpty() | ||
| new SubmittedExpenseReport(innerReportId, innerTransactionId) | ||
| })) | ||
|
|
||
| assert transactionPropagationOperations.currentTransactionId().orElseThrow() == outerTransactionId | ||
| assert expenseReportRepository.findById(innerReport.id()).orElseThrow().status == "APPROVED" | ||
|
|
||
| expenseReportService.approveReport(outerReportId) | ||
| assert transactionPropagationOperations.currentTransactionId().isEmpty() | ||
| [outerReportId, innerReport.id()] | ||
| })) | ||
|
|
||
| then: | ||
| expenseReportRepository.findById(reportIds[0]).orElseThrow().status == "APPROVED" | ||
| expenseReportRepository.findById(reportIds[1]).orElseThrow().status == "APPROVED" | ||
| } | ||
|
|
||
| private SubmittedExpenseReport submitViaHttp(String employeeId, String category, BigDecimal amount) { | ||
| HttpResponse<String> submitResponse = client.toBlocking() | ||
| .exchange(HttpRequest.POST("/expense-reports/submit/${employeeId}/${category}/${amount}", ""), String) | ||
| String transactionId = submitResponse.headers.get(SESSIONLESS_TRANSACTION_HEADER) | ||
| assert submitResponse.status == HttpStatus.OK | ||
| assert transactionId | ||
| new SubmittedExpenseReport(Long.valueOf(submitResponse.body()), transactionId) | ||
| } | ||
|
|
||
| private SubmittedExpenseReport suspendReport(String employeeId, String category, BigDecimal amount) { | ||
| Objects.requireNonNull(transactionPropagationOperations.withPropagation({ | ||
| Long reportId = expenseReportService.submitReport(employeeId, category, amount) | ||
| String transactionId = transactionPropagationOperations.currentTransactionId().orElseThrow() | ||
| new SubmittedExpenseReport(reportId, transactionId) | ||
| })) | ||
| } | ||
|
|
||
| private static final class SubmittedExpenseReport { | ||
| private final Long id | ||
| private final String transactionId | ||
|
|
||
| private SubmittedExpenseReport(Long id, String transactionId) { | ||
| this.id = id | ||
| this.transactionId = transactionId | ||
| } | ||
|
|
||
| Long id() { | ||
| id | ||
| } | ||
|
|
||
| String transactionId() { | ||
| transactionId | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Singleton | ||
| @Requires(property = "spec.name", value = OracleSessionlessTransactionPropagationSpec.SPEC_NAME) | ||
| class ExpenseReportService { | ||
|
|
||
| private final ExpenseReportRepository expenseReportRepository | ||
|
|
||
| ExpenseReportService(ExpenseReportRepository expenseReportRepository) { | ||
| this.expenseReportRepository = expenseReportRepository | ||
| } | ||
|
|
||
| @Transactional(propagation = TransactionDefinition.Propagation.SUSPEND, timeout = 3600) | ||
| Long submitReport(String employeeId, String category, BigDecimal amount) { | ||
| ExpenseReport report = expenseReportRepository.save(new ExpenseReport( | ||
| employeeId: employeeId, | ||
| category: category, | ||
| expenseAmount: amount, | ||
| status: "SUBMITTED" | ||
| )) | ||
| report.id | ||
| } | ||
|
|
||
| @Transactional(propagation = TransactionDefinition.Propagation.REQUIRES_SUSPENDED) | ||
| void approveReport(Long id) { | ||
| expenseReportRepository.updateStatus(id, "APPROVED") | ||
| } | ||
|
|
||
| @Transactional(propagation = TransactionDefinition.Propagation.REQUIRES_SUSPENDED) | ||
| void rejectReport(Long id) { | ||
| expenseReportRepository.updateStatus(id, "REJECTED") | ||
| throw new ExpenseRejectedException("Expense report failed policy check") | ||
| } | ||
| } | ||
|
|
||
| @ExecuteOn(TaskExecutors.IO) | ||
| @Controller("/expense-reports") | ||
| @Requires(property = "spec.name", value = OracleSessionlessTransactionPropagationSpec.SPEC_NAME) | ||
| class ExpenseReportController { | ||
|
|
||
| private final ExpenseReportService expenseReportService | ||
|
|
||
| ExpenseReportController(ExpenseReportService expenseReportService) { | ||
| this.expenseReportService = expenseReportService | ||
| } | ||
|
|
||
| @Post("/submit/{employeeId}/{category}/{amount}") | ||
| Long submit(String employeeId, String category, BigDecimal amount) { | ||
| expenseReportService.submitReport(employeeId, category, amount) | ||
| } | ||
|
|
||
| @Post("/approve/{id}") | ||
| @Status(HttpStatus.NO_CONTENT) | ||
| void approve(Long id) { | ||
| expenseReportService.approveReport(id) | ||
| } | ||
|
|
||
| @Post("/reject/{id}") | ||
| @Status(HttpStatus.NO_CONTENT) | ||
| void reject(Long id) { | ||
| expenseReportService.rejectReport(id) | ||
| } | ||
| } | ||
|
|
||
| @MappedEntity("expense_report") | ||
| class ExpenseReport { | ||
|
|
||
| @Id | ||
| @GeneratedValue(value = GeneratedValue.Type.SEQUENCE, ref = "EXPENSE_REPORT_SEQ") | ||
| Long id | ||
|
|
||
| String employeeId | ||
| String category | ||
| BigDecimal expenseAmount | ||
| String status | ||
| } | ||
|
|
||
| @JdbcRepository(dialect = Dialect.ORACLE) | ||
| @Requires(property = "spec.name", value = OracleSessionlessTransactionPropagationSpec.SPEC_NAME) | ||
| interface ExpenseReportRepository extends CrudRepository<ExpenseReport, Long> { | ||
|
|
||
| @Query("UPDATE expense_report SET status = :status WHERE id = :id") | ||
| void updateStatus(Long id, String status) | ||
| } | ||
|
|
||
| class ExpenseRejectedException extends RuntimeException { | ||
|
|
||
| ExpenseRejectedException(String message) { | ||
| super(message) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe suspend-resume would be implicit if Oracle session state is present?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SUSPEND and REQUIRES_SUSPENDED have just been moved out of the core propagation enum and into @OracleTransactional so the Oracle-specific behavior is explicit. I don’t think suspend/resume should be implicit from the presence of Oracle sessionless state, because
@OracleTransactionalcan also be used for other Oracle transaction options, such as transaction priority, where the transaction should still behave like a normal transaction.The propagated sessionless state should only carry the GTRID. The annotation should decide whether the method starts/suspends or resumes/completes a sessionless transaction.