Skip to content

Commit fbf56b3

Browse files
committed
chore: refactor how ServerService handles caching
1 parent a05f384 commit fbf56b3

File tree

6 files changed

+168
-14
lines changed

6 files changed

+168
-14
lines changed

src/main/java/dev/tomr/hcloud/HetznerCloud.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
public class HetznerCloud {
1616
protected static final Logger logger = LogManager.getLogger();
1717

18-
private static ListenerManager listenerManager = ListenerManager.getInstance();
19-
private static ServiceManager serviceManager = ServiceManager.getInstance();
18+
private static final ListenerManager listenerManager = ListenerManager.getInstance();
19+
private static final ServiceManager serviceManager = ServiceManager.getInstance();
2020
private static final ObjectMapper objectMapper = new ObjectMapper();
2121
private static final String HETZNER_CLOUD_HOST = "https://api.hetzner.cloud/v1/";
2222

@@ -33,7 +33,6 @@ public HetznerCloud(String apiKey) {
3333
this.apiKey = apiKey;
3434
this.host = HETZNER_CLOUD_HOST;
3535
instance = this;
36-
ServiceManager.getInstance().getServerService().forceRefreshServersCache();
3736
}
3837

3938
/**
@@ -45,7 +44,6 @@ public HetznerCloud(String apiKey, String host) {
4544
this.apiKey = apiKey;
4645
this.host = host;
4746
instance = this;
48-
ServiceManager.getInstance().getServerService().forceRefreshServersCache();
4947
}
5048

5149
/**

src/main/java/dev/tomr/hcloud/resources/server/Server.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ public class Server implements Serializable {
2222
private Iso iso;
2323
private Map<String, String> labels;
2424
private List<Object> loadBalancers; // need to figure this out
25-
private boolean locked;
25+
private Boolean locked;
2626
private String name;
2727
private PlacementGroup placementGroup;
2828
private Long primaryDiskSize;
2929
//todo change this to an actual class
3030
private List<Object> privateNet;
3131
private Protection protection;
3232
private Object publicNet;
33-
private boolean rescueEnabled;
33+
private Boolean rescueEnabled;
3434
private ServerType serverType;
3535
private String status;
3636
private List<Integer> volumes;
@@ -67,7 +67,7 @@ public Server() {
6767
* @param status Status of the Server
6868
* @param volumes Attached Volumes
6969
*/
70-
public Server(Integer id, String backupWindow, String created, Datacenter datacenter, Image image, Long includedTraffic, Long ingoingTraffic, Long outgoingTraffic, Iso iso, Map<String, String> labels, List<Object> loadBalancers, boolean locked, String name, PlacementGroup placementGroup, Long primaryDiskSize, List<Object> privateNet, Protection protection, Object publicNet, boolean rescueEnabled, ServerType serverType, String status, List<Integer> volumes) {
70+
public Server(Integer id, String backupWindow, String created, Datacenter datacenter, Image image, Long includedTraffic, Long ingoingTraffic, Long outgoingTraffic, Iso iso, Map<String, String> labels, List<Object> loadBalancers, Boolean locked, String name, PlacementGroup placementGroup, Long primaryDiskSize, List<Object> privateNet, Protection protection, Object publicNet, Boolean rescueEnabled, ServerType serverType, String status, List<Integer> volumes) {
7171
this.id = id;
7272
this.backupWindow = backupWindow;
7373
this.created = created;
@@ -177,7 +177,7 @@ public List<Object> getLoadBalancers() {
177177
return loadBalancers;
178178
}
179179

180-
public boolean isLocked() {
180+
public Boolean isLocked() {
181181
return locked;
182182
}
183183

@@ -201,7 +201,7 @@ public Object getPublicNet() {
201201
return publicNet;
202202
}
203203

204-
public boolean isRescueEnabled() {
204+
public Boolean isRescueEnabled() {
205205
return rescueEnabled;
206206
}
207207

src/main/java/dev/tomr/hcloud/service/server/ServerService.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import java.io.IOException;
1414
import java.time.Instant;
15+
import java.time.temporal.ChronoUnit;
1516
import java.util.Date;
1617
import java.util.HashMap;
1718
import java.util.List;
@@ -35,6 +36,7 @@ public class ServerService {
3536
private CompletableFuture<Void> updatedServerFuture;
3637

3738
private Map<Date, Server> remoteServers = new HashMap<>();
39+
private Date lastFullRefresh;
3840

3941
/**
4042
* Creates a new {@code ServerService} instance
@@ -109,12 +111,13 @@ private void updateAllRemoteServers() {
109111
}
110112
serverDTOList.getServers().forEach(serverDTO -> {
111113
newServerMap.put(Date.from(Instant.now()), ServerConverterUtil.transformServerDTOToServer(serverDTO));
112-
logger.info(serverDTO.getId());
113114
});
114115
remoteServers = newServerMap;
116+
lastFullRefresh = new Date();
115117
}
116118

117119
public Server getServer(Integer id) {
120+
verifyCacheUpToDate();
118121
for (var entry : remoteServers.entrySet()) {
119122
if (entry.getValue().getId().equals(id)) {
120123
return entry.getValue();
@@ -160,4 +163,10 @@ private void removeUnchangedFields(ServerDTO serverDTO) {
160163
serverDTO.setName(null);
161164
}
162165
}
166+
167+
private void verifyCacheUpToDate() {
168+
if (lastFullRefresh == null || lastFullRefresh.before(Date.from(Instant.now().minus(10, ChronoUnit.MINUTES)))) {
169+
forceRefreshServersCache();
170+
}
171+
}
163172
}

src/test/java/dev/tomr/hcloud/HetznerCloudTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package dev.tomr.hcloud;
22

33
import dev.tomr.hcloud.listener.ListenerManager;
4+
import dev.tomr.hcloud.resources.server.Server;
45
import dev.tomr.hcloud.service.ServiceManager;
6+
import dev.tomr.hcloud.service.server.ServerService;
57
import org.junit.jupiter.api.BeforeEach;
68
import org.junit.jupiter.api.DisplayName;
79
import org.junit.jupiter.api.Test;
10+
import org.mockito.MockedStatic;
811

912
import java.lang.reflect.Field;
1013
import java.util.List;
14+
import java.util.Map;
1115

1216
import static org.junit.jupiter.api.Assertions.*;
17+
import static org.mockito.Mockito.*;
1318

1419
class HetznerCloudTest {
1520

@@ -77,4 +82,78 @@ void callingGetHttpDetailsWillReturnTheHostAndApikeyInAList() {
7782
assertEquals(List.of("https://api.hetzner.cloud/v1/", "apiKey"), instance.getHttpDetails());
7883
}
7984

85+
@Test
86+
@DisplayName("Calling hasApiKey returns true for present key")
87+
void callingHasApiKeyReturnsTrueForPresentKey() {
88+
HetznerCloud instance = HetznerCloud.getInstance();
89+
instance.setApiKey("apiKey");
90+
assertTrue(instance.hasApiKey());
91+
}
92+
93+
@Test
94+
@DisplayName("Calling hasApiKey returns false for empty value")
95+
void callingHasApiKeyReturnsFalseForEmptyValue() {
96+
HetznerCloud instance = HetznerCloud.getInstance();
97+
instance.setApiKey("");
98+
assertFalse(instance.hasApiKey());
99+
}
100+
101+
@Test
102+
@DisplayName("Calling hasApiKey returns false for null value")
103+
void callingHasApiKeyReturnsFalseForNullValue() {
104+
HetznerCloud instance = HetznerCloud.getInstance();
105+
instance.setApiKey(null);
106+
assertFalse(instance.hasApiKey());
107+
}
108+
109+
@Test
110+
@DisplayName("calling getServer calls ServerManager for the server")
111+
void callingGetServerCallsServerManagerForTheServer() {
112+
try (MockedStatic<ServiceManager> serviceManagerMockedStatic = mockStatic(ServiceManager.class);
113+
MockedStatic<ListenerManager> listenerManagerMockedStatic = mockStatic(ListenerManager.class)) {
114+
115+
ServiceManager serviceManager = mock(ServiceManager.class);
116+
ServerService serverService = mock(ServerService.class);
117+
ListenerManager listenerManager = mock(ListenerManager.class);
118+
119+
serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager);
120+
listenerManagerMockedStatic.when(ListenerManager::getInstance).thenReturn(listenerManager);
121+
122+
when(serviceManager.getServerService()).thenReturn(serverService);
123+
124+
Server server = new Server(1,
125+
"backupWindow",
126+
"created",
127+
null,
128+
null,
129+
1L,
130+
1L,
131+
1L,
132+
null,
133+
Map.of("label", "value"),
134+
List.of(),
135+
false,
136+
"name",
137+
null,
138+
1L,
139+
List.of(),
140+
null,
141+
new Object(),
142+
false,
143+
null,
144+
"healthy",
145+
List.of(1));
146+
147+
HetznerCloud instance = HetznerCloud.getInstance();
148+
149+
when(serverService.getServer(anyInt())).thenReturn(server);
150+
151+
Server serverUnderTest = instance.getServer(1);
152+
153+
assertEquals(server, serverUnderTest);
154+
verify(serverService, times(1)).getServer(1);
155+
}
156+
157+
}
158+
80159
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dev.tomr.hcloud.http.model;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
public class ServerDTOListTest {
12+
13+
@Test
14+
@DisplayName("getters work as expected")
15+
void gettersWorkAsExpected() {
16+
Object o = new Object();
17+
List<ServerDTO> serverDTOList = new ArrayList<>();
18+
ServerDTOList list = new ServerDTOList(o, serverDTOList);
19+
20+
assertEquals(o, list.getMeta());
21+
assertEquals(serverDTOList, list.getServers());
22+
}
23+
24+
@Test
25+
@DisplayName("Setters work as expected")
26+
void settersWorkAsExpected() {
27+
Object o = new Object();
28+
List<ServerDTO> serverDTOList = new ArrayList<>();
29+
ServerDTOList list = new ServerDTOList();
30+
31+
list.setMeta(o);
32+
list.setServers(serverDTOList);
33+
34+
assertEquals(o, list.getMeta());
35+
assertEquals(serverDTOList, list.getServers());
36+
}
37+
}

src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dev.tomr.hcloud.http.HetznerCloudHttpClient;
55
import dev.tomr.hcloud.http.RequestVerb;
66
import dev.tomr.hcloud.http.model.ServerDTO;
7+
import dev.tomr.hcloud.http.model.ServerDTOList;
78
import dev.tomr.hcloud.listener.ListenerManager;
89
import dev.tomr.hcloud.resources.common.*;
910
import dev.tomr.hcloud.resources.server.Server;
@@ -109,7 +110,7 @@ void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTime() {
109110

110111
serverService.serverNameOrLabelUpdate("name", "name", server);
111112

112-
verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"name\":\"name\"}"));
113+
verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"name\":\"name\"}"));
113114
} catch (IOException | InterruptedException | IllegalAccessException e) {
114115
throw new RuntimeException(e);
115116
}
@@ -164,7 +165,7 @@ void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTimeWithLabels() {
164165

165166
serverService.serverNameOrLabelUpdate("labels", Map.of("label", "value"), server);
166167

167-
verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"}}"));
168+
verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"}}"));
168169
} catch (IOException | InterruptedException | IllegalAccessException e) {
169170
throw new RuntimeException(e);
170171
}
@@ -220,7 +221,7 @@ void taskUsesExtraFieldsChangedAfterFirstInvocation() {
220221
serverService.serverNameOrLabelUpdate("name", "name", server);
221222
serverService.serverNameOrLabelUpdate("labels", Map.of("l", "v"), server);
222223

223-
verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}"));
224+
verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}"));
224225
} catch (IOException | InterruptedException | IllegalAccessException e) {
225226
throw new RuntimeException(e);
226227
}
@@ -277,7 +278,7 @@ void whenHttpClientThrowsGracefully() {
277278

278279
when(hetznerCloudHttpClient.sendHttpRequest(any(), any(), any(), any(), any())).thenThrow(IOException.class);
279280

280-
verify(hetznerCloudHttpClient, timeout(2000).times(0)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}"));
281+
verify(hetznerCloudHttpClient, timeout(2000).times(0)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}"));
281282
verify(serviceManager, timeout(2000).times(1)).closeExecutor();
282283
} catch (IOException | InterruptedException | IllegalAccessException e) {
283284
throw new RuntimeException(e);
@@ -341,4 +342,34 @@ void cancelMethodPreventsTheRequestBeingSent() {
341342
throw new RuntimeException(e);
342343
}
343344
}
345+
346+
@Test
347+
@DisplayName("getServer returns a server from the cache")
348+
void getServerReturnsAServerFromTheCache() throws IOException, InterruptedException, IllegalAccessException {
349+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
350+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
351+
ListenerManager listenerManager = mock(ListenerManager.class);
352+
353+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
354+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
355+
ServerDTOList serverDTOList = new ServerDTOList();
356+
ServerDTO serverDTO = new ServerDTO();
357+
serverDTO.setName("name");
358+
serverDTO.setId(1);
359+
serverDTOList.setServers(List.of(serverDTO));
360+
361+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
362+
hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager);
363+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
364+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
365+
when(hetznerCloud.hasApiKey()).thenReturn(true);
366+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(serverDTOList);
367+
368+
ServerService serverService = new ServerService();
369+
370+
Server server = serverService.getServer(1);
371+
372+
assertEquals(serverDTO.getName(), server.getName());
373+
}
374+
}
344375
}

0 commit comments

Comments
 (0)