Skip to content

Commit

Permalink
Merge pull request #6 from Tiketeer/feat/DEV-197
Browse files Browse the repository at this point in the history
[DEV-197] OLock CreatePurchase EP 작성
  • Loading branch information
dla0510 authored Apr 22, 2024
2 parents c1bf8be + b0280af commit 605b7cb
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 24 deletions.
2 changes: 0 additions & 2 deletions src/main/java/com/tiketeer/Tiketeer/TiketeerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
@SpringBootApplication
@EnableJpaAuditing
public class TiketeerApplication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import org.springframework.web.bind.annotation.RestController;

import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchaseDLockRequestDto;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchaseOLockRequestDto;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchasePLockRequestDto;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchaseResponseDto;
import com.tiketeer.Tiketeer.domain.purchase.usecase.CreatePurchaseDLockUseCase;
import com.tiketeer.Tiketeer.domain.purchase.usecase.CreatePurchaseOLockUseCase;
import com.tiketeer.Tiketeer.domain.purchase.usecase.CreatePurchasePLockUseCase;
import com.tiketeer.Tiketeer.response.ApiResponse;

Expand All @@ -23,12 +25,14 @@ public class PurchaseController {

private final CreatePurchasePLockUseCase createPurchasePLockUseCase;
private final CreatePurchaseDLockUseCase createPurchaseDLockUseCase;
private final CreatePurchaseOLockUseCase createPurchaseOLockUseCase;

@Autowired
PurchaseController(CreatePurchasePLockUseCase createPurchasePLockUseCase,
CreatePurchaseDLockUseCase createPurchaseDLockUseCase) {
CreatePurchaseDLockUseCase createPurchaseDLockUseCase, CreatePurchaseOLockUseCase createPurchaseOLockUseCase) {
this.createPurchasePLockUseCase = createPurchasePLockUseCase;
this.createPurchaseDLockUseCase = createPurchaseDLockUseCase;
this.createPurchaseOLockUseCase = createPurchaseOLockUseCase;
}

@PostMapping("/p-lock")
Expand All @@ -46,4 +50,12 @@ public ResponseEntity<ApiResponse<PostPurchaseResponseDto>> postPurchaseWithDLoc
var responseBody = ApiResponse.wrap(PostPurchaseResponseDto.converFromDto(result));
return ResponseEntity.status(HttpStatus.CREATED).body(responseBody);
}

@PostMapping("/o-lock")
public ResponseEntity<ApiResponse<PostPurchaseResponseDto>> postPurchaseWithOLock(
@Valid @RequestBody PostPurchaseOLockRequestDto request) {
var result = createPurchaseOLockUseCase.createPurchase(request.convertToDto());
var responseBody = ApiResponse.wrap(PostPurchaseResponseDto.converFromDto(result));
return ResponseEntity.status(HttpStatus.CREATED).body(responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.tiketeer.Tiketeer.domain.purchase.controller.dto;

import java.util.UUID;

import com.tiketeer.Tiketeer.domain.purchase.usecase.dto.CreatePurchaseOLockCommandDto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@ToString
@NoArgsConstructor(force = true)
public class PostPurchaseOLockRequestDto {
@NotNull
private final UUID ticketingId;

@NotNull
private final Integer count;
@NotBlank
private final String email;
@NotNull
private final Long backoff;
@NotNull
private final Integer maxAttempts;

@Builder
public PostPurchaseOLockRequestDto(@NotNull UUID ticketingId,
@NotNull Integer count, @NotBlank String email, @NotNull Long backoff, @NotNull Integer maxAttempts) {
this.ticketingId = ticketingId;
this.count = count;
this.email = email;
this.backoff = backoff;
this.maxAttempts = maxAttempts;
}

public CreatePurchaseOLockCommandDto convertToDto() {
return CreatePurchaseOLockCommandDto.builder()
.memberEmail(email)
.ticketingId(ticketingId)
.count(count)
.backoff(backoff)
.maxAttempts(maxAttempts)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Limit;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -16,7 +15,7 @@
import com.tiketeer.Tiketeer.domain.purchase.exception.NotEnoughTicketException;
import com.tiketeer.Tiketeer.domain.purchase.repository.PurchaseRepository;
import com.tiketeer.Tiketeer.domain.purchase.service.PurchaseCrudService;
import com.tiketeer.Tiketeer.domain.purchase.usecase.dto.CreatePurchasePLockCommandDto;
import com.tiketeer.Tiketeer.domain.purchase.usecase.dto.CreatePurchaseOLockCommandDto;
import com.tiketeer.Tiketeer.domain.purchase.usecase.dto.CreatePurchaseResultDto;
import com.tiketeer.Tiketeer.domain.ticket.repository.TicketRepository;
import com.tiketeer.Tiketeer.domain.ticketing.service.TicketingService;
Expand Down Expand Up @@ -49,14 +48,11 @@ public CreatePurchaseOLockUseCase(
}

@Transactional
@Retryable(
retryFor = OptimisticLockingFailureException.class,
backoff = @Backoff(delay = 100),
maxAttempts = 100
)
public CreatePurchaseResultDto createPurchase(CreatePurchasePLockCommandDto command) {
public CreatePurchaseResultDto createPurchase(CreatePurchaseOLockCommandDto command) {
var ticketingId = command.getTicketingId();
var count = command.getCount();
var backoff = command.getBackoff();
var maxAttempts = command.getMaxAttempts();

var member = memberCrudService.findByEmail(command.getMemberEmail());

Expand All @@ -66,7 +62,15 @@ public CreatePurchaseResultDto createPurchase(CreatePurchasePLockCommandDto comm

var newPurchase = purchaseRepository.save(Purchase.builder().member(member).build());

assignPurchaseToTicket(ticketingId, newPurchase.getId(), count);
RetryTemplate.builder()
.maxAttempts(maxAttempts)
.fixedBackoff(backoff)
.retryOn(OptimisticLockingFailureException.class)
.build()
.execute(context -> {
assignPurchaseToTicket(ticketingId, newPurchase.getId(), count);
return null;
});

return CreatePurchaseResultDto.builder()
.purchaseId(newPurchase.getId())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.tiketeer.Tiketeer.domain.purchase.usecase.dto;

import java.time.LocalDateTime;
import java.util.UUID;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class CreatePurchaseOLockCommandDto {
private final String memberEmail;
private final UUID ticketingId;
private final Integer count;
private final Long backoff;
private final Integer maxAttempts;
private LocalDateTime commandCreatedAt = LocalDateTime.now();

@Builder
public CreatePurchaseOLockCommandDto(
String memberEmail, UUID ticketingId, Integer count, Long backoff, Integer maxAttempts,
LocalDateTime commandCreatedAt) {
this.memberEmail = memberEmail;
this.ticketingId = ticketingId;
this.count = count;
this.backoff = backoff;
this.maxAttempts = maxAttempts;
if (commandCreatedAt != null) {
this.commandCreatedAt = commandCreatedAt;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public class EmbeddedRedisConfig {

@PostConstruct
public void startRedis() throws IOException {
this.redisServer = RedisServer.builder().port(port).setting("maxmemory " + maxmemorySize + "M").build();
redisServer = RedisServer.builder().port(port).setting("maxmemory " + maxmemorySize + "M").build();
try {
this.redisServer.start();
redisServer.start();
log.info("레디스 서버 시작 성공");
} catch (Exception e) {
log.error("레디스 서버 시작 실패");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tiketeer.Tiketeer.domain.purchase.PurchaseTestHelper;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchaseDLockRequestDto;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchaseOLockRequestDto;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchasePLockRequestDto;
import com.tiketeer.Tiketeer.domain.purchase.controller.dto.PostPurchaseResponseDto;
import com.tiketeer.Tiketeer.domain.purchase.repository.PurchaseRepository;
Expand Down Expand Up @@ -130,4 +131,43 @@ void postPurchaseWithDLockSuccess() throws Exception {
});
}

@Test
@DisplayName("정상 컨디션 > 낙관적 락 구매 생성 요청 > 성공")
void postPurchaseWithOLockSuccess() throws Exception {
// given
var email = "[email protected]";
var seller = testHelper.createMember(email);
var ticketing = ticketingTestHelper.createTicketing(seller.getId(), 0, 1000, 5);

var buyerEmail = "[email protected]";
testHelper.createMember(buyerEmail, 100000);
var buyCnt = 2;
var req = PostPurchaseOLockRequestDto.builder()
.ticketingId(ticketing.getId())
.email(buyerEmail)
.count(buyCnt)
.backoff(100L)
.maxAttempts(100)
.build();

// when
mockMvc.perform(
post("/api/purchases/o-lock")
.contextPath("/api")
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding(StandardCharsets.UTF_8)
.content(objectMapper.writeValueAsString(req))
).andExpect(status().isCreated())
.andDo(response -> {
var result = testHelper.getDeserializedApiResponse(response.getResponse().getContentAsString(),
PostPurchaseResponseDto.class).getData();

// then
Assertions.assertThat(result.getPurchaseId()).isNotNull();
var purchase = purchaseRepository.findById(result.getPurchaseId());
Assertions.assertThat(purchase.isPresent()).isTrue();
Assertions.assertThat(ticketRepository.findAllByPurchase(purchase.get()).size()).isEqualTo(buyCnt);
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void cleanTable() {
void createPurchaseWithConcurrency() throws InterruptedException {
//given
var ticketStock = 10;
var seller = testHelper.createMember("seller@etest.com");
var seller = testHelper.createMember("seller@test.com");
var ticketing = createPurchaseConcurrencyTest.createTicketing(seller, ticketStock);

int threadNums = 20;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import org.springframework.context.annotation.Import;

import com.tiketeer.Tiketeer.domain.member.repository.MemberRepository;
import com.tiketeer.Tiketeer.domain.purchase.usecase.dto.CreatePurchasePLockCommandDto;
import com.tiketeer.Tiketeer.domain.purchase.usecase.dto.CreatePurchaseOLockCommandDto;
import com.tiketeer.Tiketeer.domain.ticket.repository.TicketRepository;
import com.tiketeer.Tiketeer.testhelper.TestHelper;
import com.tiketeer.Tiketeer.testhelper.Transaction;
Expand Down Expand Up @@ -51,18 +51,21 @@ void cleanTable() {
@DisplayName("10개의 티켓 생성 > 20명의 구매자가 경쟁 > 10명 구매 성공, 10명 구매 실패")
void createPurchaseWithConcurrency() throws InterruptedException {
//given
var seller = testHelper.createMember("[email protected]");
var ticketing = createPurchaseConcurrencyTest.createTicketing(seller, 10);
var ticketStock = 10;
var seller = testHelper.createMember("[email protected]");
var ticketing = createPurchaseConcurrencyTest.createTicketing(seller, ticketStock);

int threadNums = 20;
var buyers = createPurchaseConcurrencyTest.createBuyers(threadNums);

createPurchaseConcurrencyTest.makeConcurrency(threadNums, buyers, ticketing,
(email) -> createPurchaseOLockUseCase.createPurchase(
CreatePurchasePLockCommandDto.builder()
CreatePurchaseOLockCommandDto.builder()
.ticketingId(ticketing.getId())
.memberEmail(email)
.count(1)
.backoff(300L)
.maxAttempts(100)
.build()));

//then
Expand All @@ -72,7 +75,10 @@ void createPurchaseWithConcurrency() throws InterruptedException {
var allMembers = memberRepository.findAll();

//assert all ticket owners are unique
var ticketOwnerIdList = tickets
var purchasedTickets = ticketRepository.findAllByPurchaseIsNotNull();
assertThat(purchasedTickets.size()).isEqualTo(ticketStock);

var ticketOwnerIdList = purchasedTickets
.stream()
.map(ticket -> ticket.getPurchase().getMember().getId()).toList();

Expand All @@ -84,7 +90,7 @@ void createPurchaseWithConcurrency() throws InterruptedException {
.filter(member -> member.getPurchases().size() == 1)
.toList();

assertThat(ticketingSuccessMembers.size()).isEqualTo(10);
assertThat(ticketingSuccessMembers.size()).isEqualTo(ticketStock);
return null;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void cleanTable() {
void createPurchaseWithConcurrency() throws InterruptedException {
//given
var ticketStock = 10;
var seller = testHelper.createMember("seller@etest.com");
var seller = testHelper.createMember("seller@test.com");
var ticketing = createPurchaseConcurrencyTest.createTicketing(seller, ticketStock);

int threadNums = 20;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class CreatePurchaseUseCaseTest {
@Autowired
private TicketingTestHelper ticketingTestHelper;
@Autowired
private CreatePurchaseOLockUseCase createPurchaseUseCase;
private CreatePurchasePLockUseCase createPurchaseUseCase;
@Autowired
private TicketRepository ticketRepository;
@Autowired
Expand Down

0 comments on commit 605b7cb

Please sign in to comment.