Commit: 4617f76

Commit Details

SHA4617f76246bf039920879ae5497436d31494ed7c
Treee78bcb155880b8c167af124554a6db724c41264e
Author<f69e50@finnacloud.com> 1766442953 +0300
Committer<f69e50@finnacloud.com> 1766442953 +0300
Message
rename branch from main to master oops
GPG Signature
-----BEGIN PGP SIGNATURE-----

iQJSBAABCAA8FiEEWJb139mJI+vZ81KkoAIVSUsXI0oFAmlJx8keHHNvcGhpYS5l
cmFzbGFuQGZpbm5hY2xvdWQuY29tAAoJEKACFUlLFyNK2BQP/3EfkXtKsmQoqa4E
e9jdO5BKWUnHwF31PdpxExXwoogkZg2hSjSc0jB9htAKSbQspx+Pst7f9yj2Gf2u
ENGTEQHqqVeLEve2IPc1YJ+F+yedI3NE2PCZJ0+rh1/S14vQTT0kWMSqs6d8Te4K
x4hiTNNjfWidOXQ1vHMXl9iUnevmnko8XqNe3aBZ3JUSRLhhCvehsiPwSjfKGqB8
Sm9i1Y/HaTizFfl4WG5f6MppDgzV2I7Bm/c6K1oDIviO/Wken5vk4TXgLUWDfHJ5
d9m9gh4N9unX4Ivf5G22JVRzxPgox0Y0yFQwpj4IqQ9LzjT2Vz2s+hXoV6HZkfUW
BCLc/6oRmImcSflxOgV/TGaZrvysQ0pz32H8lyLoOI1QAS3o4u1icPFjfHlc5298
zpjGAqPjlVEx4Sjghrow4pb7sb/OyFihTwfjxlLgVfR4tXfb/l5rRt2f9vsm1HJB
qc9F/Qqc6xpb9ECyYZtuuCMToS0MRNkqy2KifrvOaKi2EznhcJu2VcLmjQJookFY
meevIkRenUvjwbs+aoDOa76HmycWkw1NtTtE9M+NSZ9SCBOD+Gp1rqO66z758bUQ
NmISh50GPDiZw5AKDrtsygWRZumLbrWHBleHm935V5O3gYkEKo9kb40Zf2sDsvlS
/wXQCTVz3XuSby3pyaMGgd3ol6gk
=YOD1
-----END PGP SIGNATURE-----

✓ Verified

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

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