Commit: f0438c2

Commit Details

SHAf0438c2cbc2d838bb66a4d8129dd25b5d48d28ee
Treeb8e53d050248c8b632fa3de86003489acae4670b
Author<f69e50@finnacloud.com> 1766443042 +0300
Committer<f69e50@finnacloud.com> 1766443042 +0300
Message
increment once more
GPG Signature
-----BEGIN PGP SIGNATURE-----

iQJSBAABCAA8FiEEWJb139mJI+vZ81KkoAIVSUsXI0oFAmlJyCIeHHNvcGhpYS5l
cmFzbGFuQGZpbm5hY2xvdWQuY29tAAoJEKACFUlLFyNKtrgP/04FZPSa4Hmx9WpR
gOamzjIXc+1+pBvGQDairLZU6rtDSgNkoryrsqOOLIXZWkkxZ70q8/aXQBFThr+t
YyDIz85u9GiwPOV8o1X4sxzF7bupOd6YmOOPqS2vco1aibpB8w9N9EwlMzW95Otw
vnQ/h0kz8MTp0wfXeRJqvLg8DVPnLgW70ly1TZQe19jEA4NwBZOyK2ksTis8iycX
HUM4WW8Velo8O+OtygAbwISr2dILKvkclTLn9kgfpN5esvBvJu4K+xA5T5alDchc
PY3FFeZteQf3GX4v0EH8K3c/q8OcQFMxuRGO31uYGqNbjbOB7zdZVs3N0B1m1efv
vhCgZ6gyxlz0kiozgO6Sx5WFOzHuTlguCK+x0JOtzCokRL/VLiPzmW5Umt280eJz
AlD7u7Dah3khoqDkzXfCL1lxch9tRMVMUYWRMayxC3iculCFM6OCLgzY9hay5y1j
WP1ZOdy/x9jKPoLv5USnw0KlH4rZIHH+aDMDEzWmWUWYDpShylG7sKafXg75bnho
5LbAfCucnDrkjN+rx1dxaKrZswQlIJ8iZ5Q6DzHnDzl4TaFcN1E3exS+xp46H7va
SbR94UfpCyjpTQ2hHdOS7XHINFUDIzWF3mUZ8gqyEANiqpgJ+RcOGX66s69oILpr
vD20SIdMHWDqJH3G6GMNsg1g5/QU
=YrcG
-----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