File History: src/main/java/com/paymentlink/reservation/ReservationService.java

← View file content

File Content at Commit 4617f76

1 package com.paymentlink.reservation;
2
3 import org.slf4j.Logger;
4 import org.slf4j.LoggerFactory;
5 import org.springframework.beans.factory.annotation.Value;
6 import org.springframework.stereotype.Service;
7
8 import java.util.concurrent.ConcurrentHashMap;
9
10 @Service
11 public class ReservationService {
12
13 private static final Logger logger = LoggerFactory.getLogger(ReservationService.class);
14
15 @Value("${reservation.timeout.ms:90000}")
16 private long reservationTimeout;
17
18 // Map: sessionId -> (productId -> Reservation)
19 private final ConcurrentHashMap<String, ConcurrentHashMap<Long, Reservation>> reservations
20 = new ConcurrentHashMap<>();
21
22 /**
23 * Reserve stock for a user's cart
24 */
25 public synchronized void reserve(String sessionId, Long productId, int quantity, int totalStock) {
26 logger.debug("Attempting to reserve {} units of product {} for session {}", quantity, productId, sessionId);
27
28 if (quantity <= 0) {
29 throw new IllegalArgumentException("Quantity must be positive");
30 }
31
32 // Calculate how much is reserved by others
33 int reservedByOthers = getReservedCountExcludingSession(productId, sessionId);
34 int availableForUser = totalStock - reservedByOthers;
35
36 logger.debug("Product {}: total stock = {}, reserved by others = {}, available = {}",
37 productId, totalStock, reservedByOthers, availableForUser);
38
39 if (availableForUser < quantity) {
40 throw new IllegalStateException(String.format(
41 "Insufficient stock. Only %d items available (you are trying to add %d)",
42 availableForUser, quantity
43 ));
44 }
45
46 // Get or create session map
47 ConcurrentHashMap<Long, Reservation> sessionMap = reservations.computeIfAbsent(
48 sessionId, k -> new ConcurrentHashMap<>()
49 );
50
51 // Create or update reservation
52 Reservation reservation = new Reservation(
53 sessionId,
54 productId,
55 quantity,
56 System.currentTimeMillis()
57 );
58 sessionMap.put(productId, reservation);
59
60 logger.info("Reserved {} units of product {} for session {}", quantity, productId, sessionId);
61 }
62
63 /**
64 * Release a reservation
65 */
66 public synchronized void release(String sessionId, Long productId) {
67 ConcurrentHashMap<Long, Reservation> sessionMap = reservations.get(sessionId);
68 if (sessionMap != null) {
69 Reservation removed = sessionMap.remove(productId);
70 if (removed != null) {
71 logger.info("Released reservation for product {} from session {}", productId, sessionId);
72 }
73 // Clean up empty session map
74 if (sessionMap.isEmpty()) {
75 reservations.remove(sessionId);
76 }
77 }
78 }
79
80 /**
81 * Get total reserved count for a product (all sessions)
82 */
83 public int getReservedCount(Long productId) {
84 int total = 0;
85 for (ConcurrentHashMap<Long, Reservation> sessionMap : reservations.values()) {
86 Reservation reservation = sessionMap.get(productId);
87 if (reservation != null && !reservation.isExpired(reservationTimeout)) {
88 total += reservation.getQuantity();
89 }
90 }
91 return total;
92 }
93
94 /**
95 * Get reserved count excluding a specific session
96 */
97 public int getReservedCountExcludingSession(Long productId, String excludeSessionId) {
98 int total = 0;
99 for (String sessionId : reservations.keySet()) {
100 if (!sessionId.equals(excludeSessionId)) {
101 ConcurrentHashMap<Long, Reservation> sessionMap = reservations.get(sessionId);
102 if (sessionMap != null) {
103 Reservation reservation = sessionMap.get(productId);
104 if (reservation != null && !reservation.isExpired(reservationTimeout)) {
105 total += reservation.getQuantity();
106 }
107 }
108 }
109 }
110 return total;
111 }
112
113 /**
114 * Get reservation for a specific session and product
115 */
116 public Integer getSessionReservation(String sessionId, Long productId) {
117 ConcurrentHashMap<Long, Reservation> sessionMap = reservations.get(sessionId);
118 if (sessionMap != null) {
119 Reservation reservation = sessionMap.get(productId);
120 if (reservation != null && !reservation.isExpired(reservationTimeout)) {
121 return reservation.getQuantity();
122 }
123 }
124 return 0;
125 }
126
127 /**
128 * Clean up expired reservations
129 */
130 public synchronized void cleanup() {
131 final java.util.concurrent.atomic.AtomicInteger cleanedCount = new java.util.concurrent.atomic.AtomicInteger(0);
132 for (String sessionId : reservations.keySet()) {
133 ConcurrentHashMap<Long, Reservation> sessionMap = reservations.get(sessionId);
134 if (sessionMap != null) {
135 sessionMap.entrySet().removeIf(entry -> {
136 boolean expired = entry.getValue().isExpired(reservationTimeout);
137 if (expired) {
138 cleanedCount.incrementAndGet();
139 logger.debug("Removing expired reservation for product {} from session {}",
140 entry.getKey(), sessionId);
141 }
142 return expired;
143 });
144 // Clean up empty session map
145 if (sessionMap.isEmpty()) {
146 reservations.remove(sessionId);
147 }
148 }
149 }
150 if (cleanedCount.get() > 0) {
151 logger.info("Cleaned up {} expired reservations", cleanedCount.get());
152 }
153 }
154
155 /**
156 * Get current timeout value (for testing/debugging)
157 */
158 public long getReservationTimeout() {
159 return reservationTimeout;
160 }
161 }
162
163

Commits

Commit Author Date Message File SHA Actions
f0438c2 <f69e50@finnacloud.com> 1766443042 +0300 12/22/2025, 10:37:22 PM increment once more 35603e8 View
188fc92 <f69e50@finnacloud.com> 1766442998 +0300 12/22/2025, 10:36:38 PM increment 35603e8 View
4617f76 <f69e50@finnacloud.com> 1766442953 +0300 12/22/2025, 10:35:53 PM rename branch from main to master oops 35603e8 Hide
e6d1548 <f69e50@finnacloud.com> 1766442769 +0300 12/22/2025, 10:32:49 PM add initial test workflow file 35603e8 View
9c24ca4 <f69e50@finnacloud.com> 1766442705 +0300 12/22/2025, 10:31:45 PM add CI configuration and test script for Jenkins build 35603e8 View
690c1f6 <f69e50@finnacloud.com> 1766368110 +0300 12/22/2025, 1:48:30 AM initialize backend structure with controllers, DTOs, and configuration files 35603e8 View