-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathConfigProvider.java
More file actions
207 lines (179 loc) · 7.07 KB
/
Copy pathConfigProvider.java
File metadata and controls
207 lines (179 loc) · 7.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package de.peeeq.wurstio.languageserver;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import de.peeeq.wurstscript.WLogger;
import org.eclipse.lsp4j.ConfigurationItem;
import org.eclipse.lsp4j.ConfigurationParams;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.services.LanguageClient;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* Provides configuration values from the language client with caching and timeout protection.
* Cache is updated asynchronously in the background to ensure changes are picked up.
*/
public class ConfigProvider {
private final LanguageClient languageClient;
// Cache with thread-safe atomic reference
private final AtomicReference<CachedConfig> cachedConfig = new AtomicReference<>(null);
// Background refresh executor
private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "ConfigProvider-Refresh");
t.setDaemon(true);
return t;
});
// Configuration timeouts
private static final long FETCH_TIMEOUT_MS = 500; // Fast timeout for blocking calls
private static final long CACHE_DURATION_MS = 5000; // 5 seconds before refresh
private static final long REFRESH_INTERVAL_MS = 10000; // Check for updates every 10 seconds
// Track if we've shown warning to avoid spam
private volatile boolean hasShownTimeoutWarning = false;
public ConfigProvider(LanguageClient languageClient) {
this.languageClient = languageClient;
// Start background refresh task
refreshExecutor.scheduleAtFixedRate(
this::refreshCacheAsync,
REFRESH_INTERVAL_MS,
REFRESH_INTERVAL_MS,
TimeUnit.MILLISECONDS
);
// Initial fetch (non-blocking)
refreshCacheAsync();
}
/**
* Get configuration value with caching and timeout protection.
* Returns cached value if available, otherwise attempts quick fetch with timeout.
*/
public String getConfig(String key, String defaultValue) {
CachedConfig cached = cachedConfig.get();
// Return cached value if valid
if (cached != null && cached.isValid()) {
String value = cached.getValue(key);
return value != null ? value : defaultValue;
}
// Try quick fetch if no valid cache
if (cached == null) {
String value = fetchConfigWithTimeout(key, defaultValue);
return value;
}
// Return stale cache while refresh happens in background
String value = cached.getValue(key);
return value != null ? value : defaultValue;
}
/**
* Fetch config with timeout protection
*/
private String fetchConfigWithTimeout(String key, String defaultValue) {
ConfigurationItem ci = new ConfigurationItem();
ci.setSection("wurst");
CompletableFuture<List<Object>> res = languageClient.configuration(
new ConfigurationParams(Collections.singletonList(ci))
);
try {
List<Object> config = res.get(FETCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
for (Object c : config) {
if (c instanceof JsonObject) {
JsonObject cfg = (JsonObject) c;
// Update cache with full config
cachedConfig.set(new CachedConfig(cfg));
JsonElement result = cfg.get(key);
if (result instanceof JsonNull) {
return null;
} else if (result != null) {
return result.getAsString();
}
}
}
return defaultValue;
} catch (TimeoutException e) {
if (!hasShownTimeoutWarning) {
WLogger.warning("Config request timed out for " + key + " after " + FETCH_TIMEOUT_MS + "ms, using default: " + defaultValue);
hasShownTimeoutWarning = true;
}
return defaultValue;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
WLogger.warning("Config request interrupted for " + key + ", using default: " + defaultValue);
return defaultValue;
} catch (ExecutionException e) {
WLogger.warning("Could not get config " + key + ", using default value " + defaultValue, e);
return defaultValue;
}
}
/**
* Asynchronously refresh the cache in background
*/
private void refreshCacheAsync() {
ConfigurationItem ci = new ConfigurationItem();
ci.setSection("wurst");
languageClient.configuration(new ConfigurationParams(Collections.singletonList(ci)))
.thenAccept(config -> {
for (Object c : config) {
if (c instanceof JsonObject) {
JsonObject cfg = (JsonObject) c;
cachedConfig.set(new CachedConfig(cfg));
WLogger.trace("Config cache refreshed successfully");
return;
}
}
})
.exceptionally(e -> {
WLogger.trace(() -> "Background config refresh failed (this is normal if client is busy): " + e.getMessage());
return null;
});
}
public String getJhcrExe() {
return getConfig("jhcrExe", "jhcr.exe");
}
public Optional<String> getWc3RunArgs() {
return Optional.ofNullable(getConfig("wc3RunArgs", null));
}
/**
* The path where to put maps before running them
*/
public Optional<String> getMapDocumentPath() {
return Optional.ofNullable(getConfig("mapDocumentPath", null));
}
/**
* Shutdown the background refresh executor
*/
public void shutdown() {
refreshExecutor.shutdown();
try {
if (!refreshExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
refreshExecutor.shutdownNow();
}
} catch (InterruptedException e) {
refreshExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* Immutable cached configuration with timestamp
*/
private static class CachedConfig {
private final JsonObject config;
private final long timestamp;
CachedConfig(JsonObject config) {
this.config = config;
this.timestamp = System.currentTimeMillis();
}
boolean isValid() {
return (System.currentTimeMillis() - timestamp) < CACHE_DURATION_MS;
}
String getValue(String key) {
JsonElement result = config.get(key);
if (result instanceof JsonNull) {
return null;
} else if (result != null) {
return result.getAsString();
}
return null;
}
}
}