Skip to content

Commit 21f73f4

Browse files
Update controllers, services, and Flask API for production features
- Update controllers to use /api/v1/* endpoints - Add idempotency key support in TeamController - Update services with Redis caching and Resilience4j - Update Flask API with Redis caching and Prometheus metrics - Update application.properties with Redis and Flyway config - Update .gitignore to exclude venv and build artifacts
1 parent 8401c61 commit 21f73f4

File tree

8 files changed

+237
-95
lines changed

8 files changed

+237
-95
lines changed

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,27 @@ yarn-error.log*
2727

2828
# backend
2929
/backend/target
30+
/backend/.mvn
31+
32+
# flask-api
3033
flask-api/FPL-Elo-Insights/
34+
flask-api/__pycache__/
35+
flask-api/*.pyc
36+
flask-api/venv/
37+
38+
# frontend
39+
/frontend/build
40+
/frontend/node_modules
41+
42+
# Environment files
43+
.env
44+
.env.local
45+
46+
# IDE
47+
*.iml
48+
.idea/
49+
.vscode/
50+
51+
# Logs
52+
logs/
53+
*.log

backend/src/main/java/com/example/demo/controller/PlayerController.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,30 @@
88
import java.util.List;
99

1010
@RestController
11-
@RequestMapping("/api/players")
11+
@RequestMapping("/api/v1/players")
1212
public class PlayerController {
1313

1414
@Autowired
1515
private PlayerDataService playerDataService;
1616

17-
// Endpoint to fetch all player data from database
17+
// Endpoint to fetch player data with basic pagination and filters
1818
@GetMapping
19-
public List<PlayerDTO> getPlayerData() {
20-
return playerDataService.getAllPlayers();
19+
public List<PlayerDTO> getPlayerData(
20+
@RequestParam(name = "page", required = false, defaultValue = "0") int page,
21+
@RequestParam(name = "size", required = false, defaultValue = "50") int size,
22+
@RequestParam(name = "team", required = false) String team,
23+
@RequestParam(name = "position", required = false) String position,
24+
@RequestParam(name = "gw", required = false) Integer gw
25+
) {
26+
List<PlayerDTO> players = playerDataService.getAllPlayers();
27+
if (team != null && !team.isEmpty()) {
28+
players = players.stream().filter(p -> team.equalsIgnoreCase(p.getTeam())).toList();
29+
}
30+
if (position != null && !position.isEmpty()) {
31+
players = players.stream().filter(p -> position.equalsIgnoreCase(p.getPosition())).toList();
32+
}
33+
int from = Math.max(0, Math.min(page * size, players.size()));
34+
int to = Math.max(0, Math.min(from + size, players.size()));
35+
return players.subList(from, to);
2136
}
2237
}

backend/src/main/java/com/example/demo/controller/TeamController.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
import com.example.demo.service.FplImportService;
1111
import com.example.demo.service.TeamOptimizationService;
1212
import com.example.demo.service.TeamService;
13+
import com.fasterxml.jackson.databind.ObjectMapper;
1314
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.data.redis.core.StringRedisTemplate;
1416
import org.springframework.http.HttpStatus;
1517
import org.springframework.http.ResponseEntity;
1618
import org.springframework.web.bind.annotation.*;
1719

1820
import java.util.List;
21+
import java.util.concurrent.TimeUnit;
1922

2023
@RestController
21-
@RequestMapping("/api/team")
24+
@RequestMapping("/api/v1/team")
2225
public class TeamController {
2326

2427
@Autowired
@@ -30,6 +33,11 @@ public class TeamController {
3033
@Autowired
3134
private TeamOptimizationService teamOptimizationService;
3235

36+
@Autowired(required = false)
37+
private StringRedisTemplate redisTemplate;
38+
39+
private final ObjectMapper objectMapper = new ObjectMapper();
40+
3341
@GetMapping
3442
public Team getTeam(@RequestParam Long userId) {
3543
return teamService.getTeamByUserId(userId);
@@ -92,11 +100,36 @@ public ResponseEntity<List<PlayerDTO>> getTeamPlayers(@RequestParam Long userId)
92100

93101
// Team optimization endpoint
94102
@PostMapping("/optimize")
95-
public ResponseEntity<OptimizeResponseDTO> optimizeTeam(@RequestParam Long userId, @RequestBody OptimizeRequestDTO request) {
103+
public ResponseEntity<OptimizeResponseDTO> optimizeTeam(@RequestParam Long userId,
104+
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
105+
@RequestBody OptimizeRequestDTO request) {
96106
try {
107+
// Idempotency check
108+
if (idempotencyKey != null && !idempotencyKey.isEmpty() && redisTemplate != null) {
109+
String cacheKey = "idempotency:" + idempotencyKey;
110+
String cachedResponse = redisTemplate.opsForValue().get(cacheKey);
111+
if (cachedResponse != null) {
112+
System.out.println("Idempotency key hit: " + idempotencyKey);
113+
OptimizeResponseDTO response = objectMapper.readValue(cachedResponse, OptimizeResponseDTO.class);
114+
return ResponseEntity.ok(response);
115+
}
116+
}
117+
97118
System.out.println("Optimization request received for user: " + userId);
98119
OptimizeResponseDTO response = teamOptimizationService.optimizeTeam(userId, request);
99120
System.out.println("Optimization completed successfully");
121+
122+
// Cache response with idempotency key (5 minute TTL)
123+
if (idempotencyKey != null && !idempotencyKey.isEmpty() && redisTemplate != null) {
124+
try {
125+
String cacheKey = "idempotency:" + idempotencyKey;
126+
String responseJson = objectMapper.writeValueAsString(response);
127+
redisTemplate.opsForValue().set(cacheKey, responseJson, 5, TimeUnit.MINUTES);
128+
} catch (Exception e) {
129+
System.err.println("Failed to cache idempotency response: " + e.getMessage());
130+
}
131+
}
132+
100133
return ResponseEntity.ok(response);
101134
} catch (Exception e) {
102135
System.err.println("Optimization error: " + e.getMessage());

backend/src/main/java/com/example/demo/controller/UserController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import java.util.List;
1212

1313
@RestController
14-
@RequestMapping("/api/users")
14+
@RequestMapping("/api/v1/auth")
1515
public class UserController {
1616

1717
@Autowired
Lines changed: 25 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,44 @@
11
package com.example.demo.dto;
22

3+
import jakarta.validation.constraints.Min;
4+
import jakarta.validation.constraints.NotNull;
35
import java.util.List;
46

57
public class OptimizeRequestDTO {
6-
private Long teamId;
7-
private Long entryId;
8-
private Double budget;
9-
private Integer freeTransfers;
10-
private List<Long> lockedPlayers;
11-
private List<Long> avoidPlayers;
12-
private String formation;
13-
14-
// Constructors
15-
public OptimizeRequestDTO() {}
16-
17-
public OptimizeRequestDTO(Long teamId, Long entryId, Double budget, Integer freeTransfers,
18-
List<Long> lockedPlayers, List<Long> avoidPlayers, String formation) {
19-
this.teamId = teamId;
20-
this.entryId = entryId;
21-
this.budget = budget;
22-
this.freeTransfers = freeTransfers;
23-
this.lockedPlayers = lockedPlayers;
24-
this.avoidPlayers = avoidPlayers;
25-
this.formation = formation;
26-
}
8+
@NotNull
9+
private Long userId;
2710

28-
// Getters and setters
29-
public Long getTeamId() {
30-
return teamId;
31-
}
32-
33-
public void setTeamId(Long teamId) {
34-
this.teamId = teamId;
35-
}
11+
@Min(0)
12+
private Double budget;
3613

37-
public Long getEntryId() {
38-
return entryId;
39-
}
14+
private Integer freeTransfers;
4015

41-
public void setEntryId(Long entryId) {
42-
this.entryId = entryId;
43-
}
16+
private List<Long> lockedPlayers;
4417

45-
public Double getBudget() {
46-
return budget;
47-
}
18+
private List<Long> avoidPlayers;
4819

49-
public void setBudget(Double budget) {
50-
this.budget = budget;
51-
}
20+
private String formation;
5221

53-
public Integer getFreeTransfers() {
54-
return freeTransfers;
55-
}
22+
private Integer gw;
5623

57-
public void setFreeTransfers(Integer freeTransfers) {
58-
this.freeTransfers = freeTransfers;
59-
}
24+
public Long getUserId() { return userId; }
25+
public void setUserId(Long userId) { this.userId = userId; }
6026

61-
public List<Long> getLockedPlayers() {
62-
return lockedPlayers;
63-
}
27+
public Double getBudget() { return budget; }
28+
public void setBudget(Double budget) { this.budget = budget; }
6429

65-
public void setLockedPlayers(List<Long> lockedPlayers) {
66-
this.lockedPlayers = lockedPlayers;
67-
}
30+
public Integer getFreeTransfers() { return freeTransfers; }
31+
public void setFreeTransfers(Integer freeTransfers) { this.freeTransfers = freeTransfers; }
6832

69-
public List<Long> getAvoidPlayers() {
70-
return avoidPlayers;
71-
}
33+
public List<Long> getLockedPlayers() { return lockedPlayers; }
34+
public void setLockedPlayers(List<Long> lockedPlayers) { this.lockedPlayers = lockedPlayers; }
7235

73-
public void setAvoidPlayers(List<Long> avoidPlayers) {
74-
this.avoidPlayers = avoidPlayers;
75-
}
36+
public List<Long> getAvoidPlayers() { return avoidPlayers; }
37+
public void setAvoidPlayers(List<Long> avoidPlayers) { this.avoidPlayers = avoidPlayers; }
7638

77-
public String getFormation() {
78-
return formation;
79-
}
39+
public String getFormation() { return formation; }
40+
public void setFormation(String formation) { this.formation = formation; }
8041

81-
public void setFormation(String formation) {
82-
this.formation = formation;
83-
}
42+
public Integer getGw() { return gw; }
43+
public void setGw(Integer gw) { this.gw = gw; }
8444
}

backend/src/main/java/com/example/demo/service/PlayerDataService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.example.demo.dto.PlayerDTO;
44
import com.example.demo.model.Player;
55
import com.example.demo.repository.PlayerRepository;
6+
import org.springframework.cache.annotation.Cacheable;
67
import org.springframework.stereotype.Service;
78

89
import java.util.List;
@@ -17,7 +18,8 @@ public PlayerDataService(PlayerRepository playerRepository) {
1718
this.playerRepository = playerRepository;
1819
}
1920

20-
// Get all players from database
21+
// Get all players from database - cached
22+
@Cacheable(value = "players", key = "'all'")
2123
public List<PlayerDTO> getAllPlayers() {
2224
List<Player> players = playerRepository.findAll();
2325
return players.stream()

backend/src/main/java/com/example/demo/service/TeamOptimizationService.java

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66
import com.example.demo.model.User;
77
import com.example.demo.repository.PlayerRepository;
88
import com.example.demo.repository.TeamRepository;
9+
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
10+
import io.github.resilience4j.retry.annotation.Retry;
11+
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
912
import org.springframework.beans.factory.annotation.Autowired;
1013
import org.springframework.beans.factory.annotation.Value;
1114
import org.springframework.http.*;
1215
import org.springframework.stereotype.Service;
1316
import org.springframework.web.client.RestTemplate;
1417
import org.springframework.http.client.SimpleClientHttpRequestFactory;
18+
import org.springframework.data.redis.core.RedisTemplate;
19+
import org.springframework.data.redis.core.StringRedisTemplate;
1520

1621
import java.util.*;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.TimeUnit;
1724
import java.util.stream.Collectors;
1825

1926
@Service
@@ -28,6 +35,9 @@ public class TeamOptimizationService {
2835
@Autowired
2936
private PlayerDataService playerDataService;
3037

38+
@Autowired(required = false)
39+
private StringRedisTemplate redisTemplate;
40+
3141
@Value("${flask.api.url:http://localhost:5001}")
3242
private String flaskApiUrl;
3343

@@ -82,22 +92,9 @@ public OptimizeResponseDTO optimizeTeam(Long userId, OptimizeRequestDTO request)
8292

8393
System.out.println("Calling Flask API at: " + flaskApiUrl + "/_ml/optimize");
8494

85-
// Call Flask ML API
86-
HttpHeaders headers = new HttpHeaders();
87-
headers.setContentType(MediaType.APPLICATION_JSON);
88-
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(flaskRequest, headers);
89-
90-
String optimizeUrl = flaskApiUrl + "/_ml/optimize";
91-
ResponseEntity<Map> response = restTemplate.postForEntity(optimizeUrl, entity, Map.class);
92-
93-
System.out.println("Flask API response status: " + response.getStatusCode());
94-
System.out.println("Flask API response body: " + response.getBody());
95-
96-
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
97-
return parseFlaskResponse(response.getBody());
98-
} else {
99-
throw new RuntimeException("Flask API returned error: " + response.getStatusCode());
100-
}
95+
// Call Flask ML API with resilience patterns
96+
Map<String, Object> flaskResponse = callFlaskOptimization(flaskRequest);
97+
return parseFlaskResponse(flaskResponse);
10198

10299
} catch (Exception e) {
103100
System.out.println("Flask API call failed, using fallback: " + e.getMessage());
@@ -107,6 +104,36 @@ public OptimizeResponseDTO optimizeTeam(Long userId, OptimizeRequestDTO request)
107104
}
108105
}
109106

107+
@CircuitBreaker(name = "flaskMl", fallbackMethod = "flaskFallback")
108+
@Retry(name = "flaskMl")
109+
@Bulkhead(name = "flaskMl")
110+
private Map<String, Object> callFlaskOptimization(Map<String, Object> flaskRequest) {
111+
HttpHeaders headers = new HttpHeaders();
112+
headers.setContentType(MediaType.APPLICATION_JSON);
113+
// Service-to-service auth header
114+
String internalToken = System.getProperty("INTERNAL_API_TOKEN",
115+
System.getenv().getOrDefault("INTERNAL_API_TOKEN", "dev-internal-token"));
116+
headers.add("X-Internal-Token", internalToken);
117+
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(flaskRequest, headers);
118+
119+
String optimizeUrl = flaskApiUrl + "/_ml/optimize";
120+
ResponseEntity<Map> response = restTemplate.postForEntity(optimizeUrl, entity, Map.class);
121+
122+
System.out.println("Flask API response status: " + response.getStatusCode());
123+
System.out.println("Flask API response body: " + response.getBody());
124+
125+
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
126+
return response.getBody();
127+
} else {
128+
throw new RuntimeException("Flask API returned error: " + response.getStatusCode());
129+
}
130+
}
131+
132+
private Map<String, Object> flaskFallback(Map<String, Object> flaskRequest, Exception e) {
133+
System.out.println("Circuit breaker fallback triggered: " + e.getMessage());
134+
throw new RuntimeException("Flask service unavailable", e);
135+
}
136+
110137
private OptimizeResponseDTO parseFlaskResponse(Map<String, Object> response) {
111138
System.out.println("Parsing Flask response: " + response);
112139

0 commit comments

Comments
 (0)