Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
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
- Loading branch information
Showing
3 changed files
with
464 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
package restx.common.watch; | ||
|
||
import com.google.common.eventbus.EventBus; | ||
|
||
import java.io.Closeable; | ||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import java.nio.file.StandardWatchEventKinds; | ||
import java.util.ArrayDeque; | ||
import java.util.Deque; | ||
import java.util.HashMap; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.ScheduledExecutorService; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* Used to coalesce {@link restx.common.watch.FileWatchEvent} in a short period of time. | ||
* | ||
* <p> | ||
* There is some cases where events will be discarded: | ||
* <ul> | ||
* <li>If the same event is posted multiple times, only the first occurrence will be kept.</li> | ||
* <li>If a create event follow a delete event, for a same file, it will be transformed into a modified event.</li> | ||
* </ul> | ||
* | ||
* @author apeyrard | ||
*/ | ||
public class FileWatchEventCoalescor implements Closeable { | ||
|
||
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); | ||
private final EventBus eventBus; | ||
private final long coalescePeriod; | ||
|
||
private final HashMap<FileWatchEventKey, Deque<EventReference>> queue = new HashMap<>(); | ||
|
||
public FileWatchEventCoalescor(EventBus eventBus, long coalescePeriod) { | ||
this.eventBus = eventBus; | ||
this.coalescePeriod = coalescePeriod; | ||
} | ||
|
||
/** | ||
* Posts a {@link restx.common.watch.FileWatchEvent}, the post will be delayed, or even discarded, if | ||
* the event might be merged, with a previous one. | ||
* | ||
* @param event the event to try to post | ||
*/ | ||
public void post(final FileWatchEvent event) { | ||
synchronized (queue) { | ||
final FileWatchEventKey key = FileWatchEventKey.fromEvent(event); | ||
|
||
Deque<EventReference> fileEvents; | ||
if ((fileEvents = queue.get(key)) == null) { | ||
// easy case, first event for a file, just queue it and schedule a post | ||
fileEvents = new ArrayDeque<>(); | ||
queue.put(key, fileEvents); | ||
EventReference reference = EventReference.of(key, event); | ||
fileEvents.add(reference); | ||
schedulePost(reference); | ||
return; | ||
} | ||
|
||
// more complex case, we need to analyze the last saved event for this file | ||
EventReference last = fileEvents.getLast(); | ||
if (!merge(last, event)) { | ||
// event has not been merged, so try to add it | ||
EventReference reference = EventReference.of(key, event); | ||
fileEvents.add(reference); | ||
schedulePost(reference); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* tries to merge the current event into the current one | ||
*/ | ||
private boolean merge(EventReference previous, FileWatchEvent current) { | ||
if (!previous.isPresent()) { | ||
return false; | ||
} | ||
|
||
if (previous.getReference().getKind() == current.getKind()) { | ||
return true; // duplicate events, keep only one | ||
} | ||
|
||
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_DELETE) { | ||
if (current.getKind() == StandardWatchEventKinds.ENTRY_CREATE) { | ||
// DELETE, then CREATE, so merge into a MODIFY | ||
previous.updateReference( | ||
FileWatchEvent.fromWithKind(previous.getReference(), StandardWatchEventKinds.ENTRY_MODIFY)); | ||
return true; | ||
} | ||
} | ||
|
||
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_CREATE) { | ||
if (current.getKind() == StandardWatchEventKinds.ENTRY_MODIFY) { | ||
// skip modify | ||
return true; | ||
} | ||
} | ||
|
||
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_CREATE) { | ||
if (current.getKind() == StandardWatchEventKinds.ENTRY_DELETE) { | ||
// CREATE then DELETE, so nothing to notify | ||
previous.clearReference(); | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* postpones the post of the specified event, when it will be time to post, | ||
* the reference might have been cleaned up | ||
* | ||
* (package-private for test purposes) | ||
*/ | ||
void schedulePost(final EventReference event) { | ||
executor.schedule(new Runnable() { | ||
@Override | ||
public void run() { | ||
synchronized (queue) { | ||
try { | ||
if (event.isPresent()) { | ||
eventBus.post(event.getReference()); | ||
} | ||
} finally { | ||
dequeue(event.getKey(), event); | ||
} | ||
} | ||
} | ||
}, coalescePeriod, TimeUnit.MILLISECONDS); | ||
} | ||
|
||
/** | ||
* remove the specified event from the queue | ||
* | ||
* (package-private for test purposes) | ||
*/ | ||
void dequeue(FileWatchEventKey key, EventReference event) { | ||
Deque<EventReference> fileEvents; | ||
if ((fileEvents = queue.get(key)) != null) { | ||
if (fileEvents.remove(event) && fileEvents.isEmpty()) { | ||
queue.remove(key); // no more events for this key, remove the stack | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* clear all events | ||
* | ||
* (package-private for test purposes) | ||
*/ | ||
void clear() { | ||
synchronized (queue) { | ||
queue.clear(); | ||
} | ||
} | ||
|
||
@Override | ||
public void close() throws IOException { | ||
executor.shutdownNow(); | ||
} | ||
|
||
/** | ||
* key used for the storage of an event, composed by file paths, two event with same keys, are for the same physical file | ||
*/ | ||
static class FileWatchEventKey { | ||
static FileWatchEventKey fromEvent(FileWatchEvent event) { | ||
return new FileWatchEventKey(event.getDir(), event.getPath()); | ||
} | ||
|
||
private final Path dir; | ||
private final Path path; | ||
|
||
private FileWatchEventKey(Path dir, Path path) { | ||
this.dir = dir; | ||
this.path = path; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) | ||
return true; | ||
if (!(o instanceof FileWatchEventKey)) | ||
return false; | ||
|
||
FileWatchEventKey that = (FileWatchEventKey) o; | ||
|
||
return dir.equals(that.dir) && path.equals(that.path); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
int result = dir.hashCode(); | ||
result = 31 * result + path.hashCode(); | ||
return result; | ||
} | ||
} | ||
|
||
/** | ||
* this is a reference holder, the reference might have been cleaned up, and be null | ||
* | ||
* it also stores the key of the event, in order to avoid key recalculation | ||
*/ | ||
static class EventReference { | ||
static EventReference of(FileWatchEventKey key, FileWatchEvent reference) { | ||
return new EventReference(key, reference); | ||
} | ||
|
||
private final FileWatchEventKey key; | ||
private FileWatchEvent reference; | ||
|
||
private EventReference(FileWatchEventKey key, FileWatchEvent reference) { | ||
this.key = key; | ||
this.reference = reference; | ||
} | ||
|
||
public void updateReference(FileWatchEvent newEvent) { | ||
reference = newEvent; | ||
} | ||
|
||
public void clearReference() { | ||
reference = null; | ||
} | ||
|
||
public boolean isPresent() { | ||
return reference != null; | ||
} | ||
|
||
public FileWatchEventKey getKey() { | ||
return key; | ||
} | ||
|
||
public FileWatchEvent getReference() { | ||
return reference; | ||
} | ||
} | ||
} |
Oops, something went wrong.