Skip to content

Commit 467fe0e

Browse files
committed
Clean up SpongeUserManager knownUUIDs opts
Fixes logic bug where the set was not being filled correctly due to trying to parse full path instead of the file name. Moved all of the logic to its own class. Correctly handle overflows and re-read the filesystem.
1 parent bc6baad commit 467fe0e

File tree

3 files changed

+253
-212
lines changed

3 files changed

+253
-212
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
* This file is part of Sponge, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) SpongePowered <https://www.spongepowered.org>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
package org.spongepowered.common.user;
26+
27+
import org.checkerframework.checker.nullness.qual.Nullable;
28+
import org.spongepowered.common.SpongeCommon;
29+
30+
import java.io.IOException;
31+
import java.nio.file.Files;
32+
import java.nio.file.Path;
33+
import java.nio.file.StandardWatchEventKinds;
34+
import java.nio.file.WatchEvent;
35+
import java.nio.file.WatchKey;
36+
import java.nio.file.WatchService;
37+
import java.util.HashMap;
38+
import java.util.HashSet;
39+
import java.util.Map;
40+
import java.util.Objects;
41+
import java.util.Set;
42+
import java.util.UUID;
43+
import java.util.function.Supplier;
44+
import java.util.stream.Collectors;
45+
import java.util.stream.Stream;
46+
47+
/**
48+
* This is an optimization for frequent calls to
49+
* retrieve all the players who have visited
50+
* the server. This could be part of tab completion
51+
* where good server performance is critical.
52+
*
53+
* While one could achieve the same with other
54+
* relevant caches, the file system is always the
55+
* source of truth.
56+
*/
57+
final class SpongeUserFileCache {
58+
59+
private final Supplier<Path> path;
60+
61+
private Set<UUID> knownUniqueIds = new HashSet<>();
62+
63+
private @Nullable WatchService watchService = null;
64+
private @Nullable WatchKey watchKey = null;
65+
66+
SpongeUserFileCache(final Supplier<Path> path) {
67+
this.path = path;
68+
}
69+
70+
public void init() {
71+
final Path path = this.path.get();
72+
this.shutdownWatcher();
73+
try {
74+
this.watchService = path.getFileSystem().newWatchService();
75+
this.watchKey = path.register(this.watchService,
76+
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
77+
} catch (final IOException e) {
78+
SpongeCommon.logger().warn("Could not start file watcher", e);
79+
this.shutdownWatcher();
80+
return;
81+
}
82+
83+
this.scanFiles(path);
84+
}
85+
86+
private void scanFiles(final Path path) {
87+
if (!Files.isDirectory(path)) {
88+
return;
89+
}
90+
91+
try (final Stream<Path> list = Files.list(path)) {
92+
this.knownUniqueIds = list.map(SpongeUserFileCache::getUniqueIdFromPath)
93+
.filter(Objects::nonNull)
94+
.collect(Collectors.toSet());
95+
} catch (final IOException e) {
96+
SpongeCommon.logger().error("Failed to get player files", e);
97+
return;
98+
}
99+
100+
this.pollFilesystemWatcher(true);
101+
}
102+
103+
private void pollFilesystemWatcher() {
104+
this.pollFilesystemWatcher(false);
105+
}
106+
107+
private void pollFilesystemWatcher(final boolean initialPoll) {
108+
if (this.watchKey == null || !this.watchKey.isValid()) {
109+
if (!initialPoll) {
110+
// Reboot this if it's somehow failed.
111+
this.init();
112+
}
113+
return;
114+
}
115+
116+
// We've already got the UUIDs, so we need to just see if the file system
117+
// watcher has found anymore (or removed any).
118+
final Map<String, MutableWatchEvent> watcherUpdateMap = new HashMap<>();
119+
for (final WatchEvent<?> event : this.watchKey.pollEvents()) {
120+
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
121+
if (!initialPoll) {
122+
this.scanFiles(this.path.get());
123+
} else {
124+
this.watchKey.cancel();
125+
}
126+
return;
127+
}
128+
129+
@SuppressWarnings("unchecked") final WatchEvent<Path> ev = (WatchEvent<Path>) event;
130+
final @Nullable Path file = ev.context();
131+
132+
// It is possible that the context is null, in which case, ignore it.
133+
if (file != null) {
134+
final String filename = file.getFileName().toString();
135+
136+
// We don't determine the UUIDs yet, we'll only do that if we need to.
137+
watcherUpdateMap.computeIfAbsent(filename, f -> new MutableWatchEvent()).set(ev.kind());
138+
}
139+
}
140+
141+
// Now we know what the final result is, we can act upon it.
142+
for (final Map.Entry<String, MutableWatchEvent> entry : watcherUpdateMap.entrySet()) {
143+
final WatchEvent.Kind<?> kind = entry.getValue().get();
144+
if (kind == null) {
145+
continue;
146+
}
147+
148+
final @Nullable UUID uuid = SpongeUserFileCache.getUniqueIdFromPath(entry.getKey());
149+
if (uuid == null) {
150+
continue;
151+
}
152+
153+
// It will only be create or delete here.
154+
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
155+
this.knownUniqueIds.add(uuid);
156+
} else {
157+
this.knownUniqueIds.remove(uuid);
158+
}
159+
}
160+
}
161+
162+
public void userCreated(final UUID uniqueId) {
163+
this.pollFilesystemWatcher();
164+
this.knownUniqueIds.add(uniqueId);
165+
}
166+
167+
public boolean contains(final UUID uniqueId) {
168+
this.pollFilesystemWatcher();
169+
return this.knownUniqueIds.contains(uniqueId);
170+
}
171+
172+
public Stream<UUID> knownUUIDs() {
173+
this.pollFilesystemWatcher();
174+
return this.knownUniqueIds.stream();
175+
}
176+
177+
public void shutdownWatcher() {
178+
if (this.watchKey != null) {
179+
this.watchKey.cancel();
180+
this.watchKey = null;
181+
}
182+
183+
if (this.watchService != null) {
184+
try {
185+
this.watchService.close();
186+
} catch (final IOException ignored) {
187+
}
188+
189+
this.watchService = null;
190+
}
191+
}
192+
193+
private static @Nullable UUID getUniqueIdFromPath(final Path path) {
194+
return SpongeUserFileCache.getUniqueIdFromPath(path.getFileName().toString());
195+
}
196+
197+
private static @Nullable UUID getUniqueIdFromPath(final String fileName) {
198+
final String[] parts = fileName.split("\\.", 2);
199+
if (parts.length != 2 || parts[0].length() != 36 || !parts[1].equals("dat")) {
200+
return null;
201+
}
202+
try {
203+
return UUID.fromString(parts[0]);
204+
} catch (final IllegalArgumentException ignored) {
205+
return null;
206+
}
207+
}
208+
209+
/**
210+
* Filters that sequences of CREATE -> DELETE
211+
* or DELETE -> CREATE do not raise changes.
212+
*/
213+
private static final class MutableWatchEvent {
214+
215+
private WatchEvent.Kind<?> kind = null;
216+
217+
public WatchEvent.@Nullable Kind<?> get() {
218+
return this.kind;
219+
}
220+
221+
public void set(WatchEvent.Kind<?> kind) {
222+
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
223+
// This should never happen, we don't listen to this.
224+
// However, if it does, treat it as a create, because it
225+
// infers the existence of the file.
226+
kind = StandardWatchEventKinds.ENTRY_CREATE;
227+
}
228+
229+
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_DELETE) {
230+
if (this.kind != null && this.kind != kind) {
231+
this.kind = null;
232+
} else {
233+
this.kind = kind;
234+
}
235+
}
236+
}
237+
}
238+
}

0 commit comments

Comments
 (0)