Skip to content

Commit 59454c0

Browse files
committed
Introduces a new coalescor for FileWatchEvent only
This coalescor will merge events for a same file, not only if the event is the same than the previous one, but also with some rules: - DELETE + CREATE => MODIFY - CREATE + MODIFY => CREATE - CREATE + DELETE => discarded
1 parent 68381e5 commit 59454c0

File tree

3 files changed

+464
-0
lines changed

3 files changed

+464
-0
lines changed

restx-common/src/main/java/restx/common/watch/FileWatchEvent.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ public static FileWatchEvent newInstance(Path root, Path dir, Path path, WatchEv
1313
return new FileWatchEvent(root, normalizePath(root, dir.resolve(normalizePath(dir, path))), kind, count);
1414
}
1515

16+
/**
17+
* Create a new {@link FileWatchEvent} from a reference, and apply the new specified kind.
18+
* @param ref the reference
19+
* @param newKind the new kind
20+
* @return the created event
21+
*/
22+
public static FileWatchEvent fromWithKind(FileWatchEvent ref, WatchEvent.Kind<?> newKind) {
23+
return new FileWatchEvent(ref.dir, ref.path, newKind, ref.count);
24+
}
25+
1626
private final Path dir;
1727
private final Path path;
1828
private final WatchEvent.Kind<?> kind;
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package restx.common.watch;
2+
3+
import com.google.common.eventbus.EventBus;
4+
5+
import java.io.Closeable;
6+
import java.io.IOException;
7+
import java.nio.file.Path;
8+
import java.nio.file.StandardWatchEventKinds;
9+
import java.util.ArrayDeque;
10+
import java.util.Deque;
11+
import java.util.HashMap;
12+
import java.util.concurrent.Executors;
13+
import java.util.concurrent.ScheduledExecutorService;
14+
import java.util.concurrent.TimeUnit;
15+
16+
/**
17+
* Used to coalesce {@link restx.common.watch.FileWatchEvent} in a short period of time.
18+
*
19+
* <p>
20+
* There is some cases where events will be discarded:
21+
* <ul>
22+
* <li>If the same event is posted multiple times, only the first occurrence will be kept.</li>
23+
* <li>If a create event follow a delete event, for a same file, it will be transformed into a modified event.</li>
24+
* </ul>
25+
*
26+
* @author apeyrard
27+
*/
28+
public class FileWatchEventCoalescor implements Closeable {
29+
30+
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
31+
private final EventBus eventBus;
32+
private final long coalescePeriod;
33+
34+
private final HashMap<FileWatchEventKey, Deque<EventReference>> queue = new HashMap<>();
35+
36+
public FileWatchEventCoalescor(EventBus eventBus, long coalescePeriod) {
37+
this.eventBus = eventBus;
38+
this.coalescePeriod = coalescePeriod;
39+
}
40+
41+
/**
42+
* Posts a {@link restx.common.watch.FileWatchEvent}, the post will be delayed, or even discarded, if
43+
* the event might be merged, with a previous one.
44+
*
45+
* @param event the event to try to post
46+
*/
47+
public void post(final FileWatchEvent event) {
48+
synchronized (queue) {
49+
final FileWatchEventKey key = FileWatchEventKey.fromEvent(event);
50+
51+
Deque<EventReference> fileEvents;
52+
if ((fileEvents = queue.get(key)) == null) {
53+
// easy case, first event for a file, just queue it and schedule a post
54+
fileEvents = new ArrayDeque<>();
55+
queue.put(key, fileEvents);
56+
EventReference reference = EventReference.of(key, event);
57+
fileEvents.add(reference);
58+
schedulePost(reference);
59+
return;
60+
}
61+
62+
// more complex case, we need to analyze the last saved event for this file
63+
EventReference last = fileEvents.getLast();
64+
if (!merge(last, event)) {
65+
// event has not been merged, so try to add it
66+
EventReference reference = EventReference.of(key, event);
67+
fileEvents.add(reference);
68+
schedulePost(reference);
69+
}
70+
}
71+
}
72+
73+
/**
74+
* tries to merge the current event into the current one
75+
*/
76+
private boolean merge(EventReference previous, FileWatchEvent current) {
77+
if (!previous.isPresent()) {
78+
return false;
79+
}
80+
81+
if (previous.getReference().getKind() == current.getKind()) {
82+
return true; // duplicate events, keep only one
83+
}
84+
85+
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_DELETE) {
86+
if (current.getKind() == StandardWatchEventKinds.ENTRY_CREATE) {
87+
// DELETE, then CREATE, so merge into a MODIFY
88+
previous.updateReference(
89+
FileWatchEvent.fromWithKind(previous.getReference(), StandardWatchEventKinds.ENTRY_MODIFY));
90+
return true;
91+
}
92+
}
93+
94+
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_CREATE) {
95+
if (current.getKind() == StandardWatchEventKinds.ENTRY_MODIFY) {
96+
// skip modify
97+
return true;
98+
}
99+
}
100+
101+
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_CREATE) {
102+
if (current.getKind() == StandardWatchEventKinds.ENTRY_DELETE) {
103+
// CREATE then DELETE, so nothing to notify
104+
previous.clearReference();
105+
return true;
106+
}
107+
}
108+
109+
return false;
110+
}
111+
112+
/**
113+
* postpones the post of the specified event, when it will be time to post,
114+
* the reference might have been cleaned up
115+
*
116+
* (package-private for test purposes)
117+
*/
118+
void schedulePost(final EventReference event) {
119+
executor.schedule(new Runnable() {
120+
@Override
121+
public void run() {
122+
synchronized (queue) {
123+
try {
124+
if (event.isPresent()) {
125+
eventBus.post(event.getReference());
126+
}
127+
} finally {
128+
dequeue(event.getKey(), event);
129+
}
130+
}
131+
}
132+
}, coalescePeriod, TimeUnit.MILLISECONDS);
133+
}
134+
135+
/**
136+
* remove the specified event from the queue
137+
*
138+
* (package-private for test purposes)
139+
*/
140+
void dequeue(FileWatchEventKey key, EventReference event) {
141+
Deque<EventReference> fileEvents;
142+
if ((fileEvents = queue.get(key)) != null) {
143+
if (fileEvents.remove(event) && fileEvents.isEmpty()) {
144+
queue.remove(key); // no more events for this key, remove the stack
145+
}
146+
}
147+
}
148+
149+
/**
150+
* clear all events
151+
*
152+
* (package-private for test purposes)
153+
*/
154+
void clear() {
155+
synchronized (queue) {
156+
queue.clear();
157+
}
158+
}
159+
160+
@Override
161+
public void close() throws IOException {
162+
executor.shutdownNow();
163+
}
164+
165+
/**
166+
* key used for the storage of an event, composed by file paths, two event with same keys, are for the same physical file
167+
*/
168+
static class FileWatchEventKey {
169+
static FileWatchEventKey fromEvent(FileWatchEvent event) {
170+
return new FileWatchEventKey(event.getDir(), event.getPath());
171+
}
172+
173+
private final Path dir;
174+
private final Path path;
175+
176+
private FileWatchEventKey(Path dir, Path path) {
177+
this.dir = dir;
178+
this.path = path;
179+
}
180+
181+
@Override
182+
public boolean equals(Object o) {
183+
if (this == o)
184+
return true;
185+
if (!(o instanceof FileWatchEventKey))
186+
return false;
187+
188+
FileWatchEventKey that = (FileWatchEventKey) o;
189+
190+
return dir.equals(that.dir) && path.equals(that.path);
191+
}
192+
193+
@Override
194+
public int hashCode() {
195+
int result = dir.hashCode();
196+
result = 31 * result + path.hashCode();
197+
return result;
198+
}
199+
}
200+
201+
/**
202+
* this is a reference holder, the reference might have been cleaned up, and be null
203+
*
204+
* it also stores the key of the event, in order to avoid key recalculation
205+
*/
206+
static class EventReference {
207+
static EventReference of(FileWatchEventKey key, FileWatchEvent reference) {
208+
return new EventReference(key, reference);
209+
}
210+
211+
private final FileWatchEventKey key;
212+
private FileWatchEvent reference;
213+
214+
private EventReference(FileWatchEventKey key, FileWatchEvent reference) {
215+
this.key = key;
216+
this.reference = reference;
217+
}
218+
219+
public void updateReference(FileWatchEvent newEvent) {
220+
reference = newEvent;
221+
}
222+
223+
public void clearReference() {
224+
reference = null;
225+
}
226+
227+
public boolean isPresent() {
228+
return reference != null;
229+
}
230+
231+
public FileWatchEventKey getKey() {
232+
return key;
233+
}
234+
235+
public FileWatchEvent getReference() {
236+
return reference;
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)