mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-10 15:20:56 +00:00
439 lines
13 KiB
Go
439 lines
13 KiB
Go
|
// Copyright (C) 2016 The Syncthing Authors.
|
||
|
//
|
||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
|
|
||
|
package watchaggregator
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/syncthing/syncthing/lib/config"
|
||
|
"github.com/syncthing/syncthing/lib/events"
|
||
|
"github.com/syncthing/syncthing/lib/fs"
|
||
|
)
|
||
|
|
||
|
// Not meant to be changed, but must be changeable for tests
|
||
|
var (
|
||
|
maxFiles = 512
|
||
|
maxFilesPerDir = 128
|
||
|
)
|
||
|
|
||
|
// aggregatedEvent represents potentially multiple events at and/or recursively
|
||
|
// below one path until it times out and a scan is scheduled.
|
||
|
type aggregatedEvent struct {
|
||
|
firstModTime time.Time
|
||
|
lastModTime time.Time
|
||
|
evType fs.EventType
|
||
|
}
|
||
|
|
||
|
// Stores pointers to both aggregated events directly within this directory and
|
||
|
// child directories recursively containing aggregated events themselves.
|
||
|
type eventDir struct {
|
||
|
events map[string]*aggregatedEvent
|
||
|
dirs map[string]*eventDir
|
||
|
}
|
||
|
|
||
|
func newEventDir() *eventDir {
|
||
|
return &eventDir{
|
||
|
events: make(map[string]*aggregatedEvent),
|
||
|
dirs: make(map[string]*eventDir),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (dir *eventDir) eventCount() int {
|
||
|
count := len(dir.events)
|
||
|
for _, dir := range dir.dirs {
|
||
|
count += dir.eventCount()
|
||
|
}
|
||
|
return count
|
||
|
}
|
||
|
|
||
|
func (dir *eventDir) childCount() int {
|
||
|
return len(dir.events) + len(dir.dirs)
|
||
|
}
|
||
|
|
||
|
func (dir *eventDir) firstModTime() time.Time {
|
||
|
if dir.childCount() == 0 {
|
||
|
panic("bug: firstModTime must not be used on empty eventDir")
|
||
|
}
|
||
|
firstModTime := time.Now()
|
||
|
for _, childDir := range dir.dirs {
|
||
|
dirTime := childDir.firstModTime()
|
||
|
if dirTime.Before(firstModTime) {
|
||
|
firstModTime = dirTime
|
||
|
}
|
||
|
}
|
||
|
for _, event := range dir.events {
|
||
|
if event.firstModTime.Before(firstModTime) {
|
||
|
firstModTime = event.firstModTime
|
||
|
}
|
||
|
}
|
||
|
return firstModTime
|
||
|
}
|
||
|
|
||
|
func (dir *eventDir) eventType() fs.EventType {
|
||
|
if dir.childCount() == 0 {
|
||
|
panic("bug: eventType must not be used on empty eventDir")
|
||
|
}
|
||
|
var evType fs.EventType
|
||
|
for _, childDir := range dir.dirs {
|
||
|
evType |= childDir.eventType()
|
||
|
if evType == fs.Mixed {
|
||
|
return fs.Mixed
|
||
|
}
|
||
|
}
|
||
|
for _, event := range dir.events {
|
||
|
evType |= event.evType
|
||
|
if evType == fs.Mixed {
|
||
|
return fs.Mixed
|
||
|
}
|
||
|
}
|
||
|
return evType
|
||
|
}
|
||
|
|
||
|
type aggregator struct {
|
||
|
folderCfg config.FolderConfiguration
|
||
|
folderCfgUpdate chan config.FolderConfiguration
|
||
|
// Time after which an event is scheduled for scanning when no modifications occur.
|
||
|
notifyDelay time.Duration
|
||
|
// Time after which an event is scheduled for scanning even though modifications occur.
|
||
|
notifyTimeout time.Duration
|
||
|
notifyTimer *time.Timer
|
||
|
notifyTimerNeedsReset bool
|
||
|
notifyTimerResetChan chan time.Duration
|
||
|
ctx context.Context
|
||
|
}
|
||
|
|
||
|
func new(folderCfg config.FolderConfiguration, ctx context.Context) *aggregator {
|
||
|
a := &aggregator{
|
||
|
folderCfgUpdate: make(chan config.FolderConfiguration),
|
||
|
notifyTimerNeedsReset: false,
|
||
|
notifyTimerResetChan: make(chan time.Duration),
|
||
|
ctx: ctx,
|
||
|
}
|
||
|
|
||
|
a.updateConfig(folderCfg)
|
||
|
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
func Aggregate(in <-chan fs.Event, out chan<- []string, folderCfg config.FolderConfiguration, cfg *config.Wrapper, ctx context.Context) {
|
||
|
a := new(folderCfg, ctx)
|
||
|
|
||
|
// Necessary for unit tests where the backend is mocked
|
||
|
go a.mainLoop(in, out, cfg)
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) mainLoop(in <-chan fs.Event, out chan<- []string, cfg *config.Wrapper) {
|
||
|
a.notifyTimer = time.NewTimer(a.notifyDelay)
|
||
|
defer a.notifyTimer.Stop()
|
||
|
|
||
|
inProgress := make(map[string]struct{})
|
||
|
inProgressItemSubscription := events.Default.Subscribe(events.ItemStarted | events.ItemFinished)
|
||
|
|
||
|
cfg.Subscribe(a)
|
||
|
|
||
|
rootEventDir := newEventDir()
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case event := <-in:
|
||
|
a.newEvent(event, rootEventDir, inProgress)
|
||
|
case event := <-inProgressItemSubscription.C():
|
||
|
updateInProgressSet(event, inProgress)
|
||
|
case <-a.notifyTimer.C:
|
||
|
a.actOnTimer(rootEventDir, out)
|
||
|
case interval := <-a.notifyTimerResetChan:
|
||
|
a.resetNotifyTimer(interval)
|
||
|
case folderCfg := <-a.folderCfgUpdate:
|
||
|
a.updateConfig(folderCfg)
|
||
|
case <-a.ctx.Done():
|
||
|
cfg.Unsubscribe(a)
|
||
|
l.Debugln(a, "Stopped")
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) newEvent(event fs.Event, rootEventDir *eventDir, inProgress map[string]struct{}) {
|
||
|
if _, ok := rootEventDir.events["."]; ok {
|
||
|
l.Debugln(a, "Will scan entire folder anyway; dropping:", event.Name)
|
||
|
return
|
||
|
}
|
||
|
if _, ok := inProgress[event.Name]; ok {
|
||
|
l.Debugln(a, "Skipping path we modified:", event.Name)
|
||
|
return
|
||
|
}
|
||
|
a.aggregateEvent(event, time.Now(), rootEventDir)
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) aggregateEvent(event fs.Event, evTime time.Time, rootEventDir *eventDir) {
|
||
|
if event.Name == "." || rootEventDir.eventCount() == maxFiles {
|
||
|
l.Debugln(a, "Scan entire folder")
|
||
|
firstModTime := evTime
|
||
|
if rootEventDir.childCount() != 0 {
|
||
|
event.Type |= rootEventDir.eventType()
|
||
|
firstModTime = rootEventDir.firstModTime()
|
||
|
}
|
||
|
rootEventDir.dirs = make(map[string]*eventDir)
|
||
|
rootEventDir.events = make(map[string]*aggregatedEvent)
|
||
|
rootEventDir.events["."] = &aggregatedEvent{
|
||
|
firstModTime: firstModTime,
|
||
|
lastModTime: evTime,
|
||
|
evType: event.Type,
|
||
|
}
|
||
|
a.resetNotifyTimerIfNeeded()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
parentDir := rootEventDir
|
||
|
|
||
|
// Check if any parent directory is already tracked or will exceed
|
||
|
// events per directory limit bottom up
|
||
|
pathSegments := strings.Split(filepath.ToSlash(event.Name), "/")
|
||
|
|
||
|
// As root dir cannot be further aggregated, allow up to maxFiles
|
||
|
// children.
|
||
|
localMaxFilesPerDir := maxFiles
|
||
|
var currPath string
|
||
|
for i, name := range pathSegments[:len(pathSegments)-1] {
|
||
|
currPath = filepath.Join(currPath, name)
|
||
|
|
||
|
if ev, ok := parentDir.events[name]; ok {
|
||
|
ev.lastModTime = evTime
|
||
|
ev.evType |= event.Type
|
||
|
l.Debugf("%v Parent %s (type %s) already tracked: %s", a, currPath, ev.evType, event.Name)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if parentDir.childCount() == localMaxFilesPerDir {
|
||
|
l.Debugf("%v Parent dir %s already has %d children, tracking it instead: %s", a, currPath, localMaxFilesPerDir, event.Name)
|
||
|
event.Name = filepath.Dir(currPath)
|
||
|
a.aggregateEvent(event, evTime, rootEventDir)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// If there are no events below path, but we need to recurse
|
||
|
// into that path, create eventDir at path.
|
||
|
if newParent, ok := parentDir.dirs[name]; ok {
|
||
|
parentDir = newParent
|
||
|
} else {
|
||
|
l.Debugln(a, "Creating eventDir at:", currPath)
|
||
|
newParent = newEventDir()
|
||
|
parentDir.dirs[name] = newParent
|
||
|
parentDir = newParent
|
||
|
}
|
||
|
|
||
|
// Reset allowed children count to maxFilesPerDir for non-root
|
||
|
if i == 0 {
|
||
|
localMaxFilesPerDir = maxFilesPerDir
|
||
|
}
|
||
|
}
|
||
|
|
||
|
name := pathSegments[len(pathSegments)-1]
|
||
|
|
||
|
if ev, ok := parentDir.events[name]; ok {
|
||
|
ev.lastModTime = evTime
|
||
|
ev.evType |= event.Type
|
||
|
l.Debugf("%v Already tracked (type %v): %s", a, ev.evType, event.Name)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
childDir, ok := parentDir.dirs[name]
|
||
|
|
||
|
// If a dir existed at path, it would be removed from dirs, thus
|
||
|
// childCount would not increase.
|
||
|
if !ok && parentDir.childCount() == localMaxFilesPerDir {
|
||
|
l.Debugf("%v Parent dir already has %d children, tracking it instead: %s", a, localMaxFilesPerDir, event.Name)
|
||
|
event.Name = filepath.Dir(event.Name)
|
||
|
a.aggregateEvent(event, evTime, rootEventDir)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
firstModTime := evTime
|
||
|
if ok {
|
||
|
firstModTime = childDir.firstModTime()
|
||
|
event.Type |= childDir.eventType()
|
||
|
delete(parentDir.dirs, name)
|
||
|
}
|
||
|
l.Debugf("%v Tracking (type %v): %s", a, event.Type, event.Name)
|
||
|
parentDir.events[name] = &aggregatedEvent{
|
||
|
firstModTime: firstModTime,
|
||
|
lastModTime: evTime,
|
||
|
evType: event.Type,
|
||
|
}
|
||
|
a.resetNotifyTimerIfNeeded()
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) resetNotifyTimerIfNeeded() {
|
||
|
if a.notifyTimerNeedsReset {
|
||
|
a.resetNotifyTimer(a.notifyDelay)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// resetNotifyTimer should only ever be called when notifyTimer has stopped
|
||
|
// and notifyTimer.C been read from. Otherwise, call resetNotifyTimerIfNeeded.
|
||
|
func (a *aggregator) resetNotifyTimer(duration time.Duration) {
|
||
|
l.Debugln(a, "Resetting notifyTimer to", duration.String())
|
||
|
a.notifyTimerNeedsReset = false
|
||
|
a.notifyTimer.Reset(duration)
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) actOnTimer(rootEventDir *eventDir, out chan<- []string) {
|
||
|
eventCount := rootEventDir.eventCount()
|
||
|
if eventCount == 0 {
|
||
|
l.Debugln(a, "No tracked events, waiting for new event.")
|
||
|
a.notifyTimerNeedsReset = true
|
||
|
return
|
||
|
}
|
||
|
oldevents := a.popOldEvents(rootEventDir, ".", time.Now())
|
||
|
if len(oldevents) == 0 {
|
||
|
l.Debugln(a, "No old fs events")
|
||
|
a.resetNotifyTimer(a.notifyDelay)
|
||
|
return
|
||
|
}
|
||
|
// Sending to channel might block for a long time, but we need to keep
|
||
|
// reading from notify backend channel to avoid overflow
|
||
|
go a.notify(oldevents, out)
|
||
|
}
|
||
|
|
||
|
// Schedule scan for given events dispatching deletes last and reset notification
|
||
|
// afterwards to set up for the next scan scheduling.
|
||
|
func (a *aggregator) notify(oldEvents map[string]*aggregatedEvent, out chan<- []string) {
|
||
|
timeBeforeSending := time.Now()
|
||
|
l.Debugf("%v Notifying about %d fs events", a, len(oldEvents))
|
||
|
separatedBatches := make(map[fs.EventType][]string)
|
||
|
for path, event := range oldEvents {
|
||
|
separatedBatches[event.evType] = append(separatedBatches[event.evType], path)
|
||
|
}
|
||
|
for _, evType := range [3]fs.EventType{fs.NonRemove, fs.Mixed, fs.Remove} {
|
||
|
currBatch := separatedBatches[evType]
|
||
|
if len(currBatch) != 0 {
|
||
|
select {
|
||
|
case out <- currBatch:
|
||
|
case <-a.ctx.Done():
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// If sending to channel blocked for a long time,
|
||
|
// shorten next notifyDelay accordingly.
|
||
|
duration := time.Since(timeBeforeSending)
|
||
|
buffer := time.Millisecond
|
||
|
var nextDelay time.Duration
|
||
|
switch {
|
||
|
case duration < a.notifyDelay/10:
|
||
|
nextDelay = a.notifyDelay
|
||
|
case duration+buffer > a.notifyDelay:
|
||
|
nextDelay = buffer
|
||
|
default:
|
||
|
nextDelay = a.notifyDelay - duration
|
||
|
}
|
||
|
select {
|
||
|
case a.notifyTimerResetChan <- nextDelay:
|
||
|
case <-a.ctx.Done():
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// popOldEvents finds events that should be scheduled for scanning recursively in dirs,
|
||
|
// removes those events and empty eventDirs and returns a map with all the removed
|
||
|
// events referenced by their filesystem path
|
||
|
func (a *aggregator) popOldEvents(dir *eventDir, dirPath string, currTime time.Time) map[string]*aggregatedEvent {
|
||
|
oldEvents := make(map[string]*aggregatedEvent)
|
||
|
for childName, childDir := range dir.dirs {
|
||
|
for evPath, event := range a.popOldEvents(childDir, filepath.Join(dirPath, childName), currTime) {
|
||
|
oldEvents[evPath] = event
|
||
|
}
|
||
|
if childDir.childCount() == 0 {
|
||
|
delete(dir.dirs, childName)
|
||
|
}
|
||
|
}
|
||
|
for name, event := range dir.events {
|
||
|
if a.isOld(event, currTime) {
|
||
|
oldEvents[filepath.Join(dirPath, name)] = event
|
||
|
delete(dir.events, name)
|
||
|
}
|
||
|
}
|
||
|
return oldEvents
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) isOld(ev *aggregatedEvent, currTime time.Time) bool {
|
||
|
// Deletes should always be scanned last, therefore they are always
|
||
|
// delayed by letting them time out (see below).
|
||
|
// An event that has not registered any new modifications recently is scanned.
|
||
|
// a.notifyDelay is the user facing value signifying the normal delay between
|
||
|
// a picking up a modification and scanning it. As scheduling scans happens at
|
||
|
// regular intervals of a.notifyDelay the delay of a single event is not exactly
|
||
|
// a.notifyDelay, but lies in in the range of 0.5 to 1.5 times a.notifyDelay.
|
||
|
if ev.evType == fs.NonRemove && 2*currTime.Sub(ev.lastModTime) > a.notifyDelay {
|
||
|
return true
|
||
|
}
|
||
|
// When an event registers repeat modifications or involves removals it
|
||
|
// is delayed to reduce resource usage, but after a certain time (notifyTimeout)
|
||
|
// passed it is scanned anyway.
|
||
|
return currTime.Sub(ev.firstModTime) > a.notifyTimeout
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) String() string {
|
||
|
return fmt.Sprintf("aggregator/%s:", a.folderCfg.Description())
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) VerifyConfiguration(from, to config.Configuration) error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) CommitConfiguration(from, to config.Configuration) bool {
|
||
|
for _, folderCfg := range to.Folders {
|
||
|
if folderCfg.ID == a.folderCfg.ID {
|
||
|
select {
|
||
|
case a.folderCfgUpdate <- folderCfg:
|
||
|
case <-a.ctx.Done():
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
// Nothing to do, model will soon stop this
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (a *aggregator) updateConfig(folderCfg config.FolderConfiguration) {
|
||
|
a.notifyDelay = time.Duration(folderCfg.FSWatcherDelayS) * time.Second
|
||
|
a.notifyTimeout = notifyTimeout(folderCfg.FSWatcherDelayS)
|
||
|
a.folderCfg = folderCfg
|
||
|
}
|
||
|
|
||
|
func updateInProgressSet(event events.Event, inProgress map[string]struct{}) {
|
||
|
if event.Type == events.ItemStarted {
|
||
|
path := event.Data.(map[string]string)["item"]
|
||
|
inProgress[path] = struct{}{}
|
||
|
} else if event.Type == events.ItemFinished {
|
||
|
path := event.Data.(map[string]interface{})["item"].(string)
|
||
|
delete(inProgress, path)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Events that involve removals or continuously receive new modifications are
|
||
|
// delayed but must time out at some point. The following numbers come out of thin
|
||
|
// air, they were just considered as a sensible compromise between fast updates and
|
||
|
// saving resources. For short delays the timeout is 6 times the delay, capped at 1
|
||
|
// minute. For delays longer than 1 minute, the delay and timeout are equal.
|
||
|
func notifyTimeout(eventDelayS int) time.Duration {
|
||
|
shortDelayS := 10
|
||
|
shortDelayMultiplicator := 6
|
||
|
longDelayS := 60
|
||
|
longDelayTimeout := time.Duration(1) * time.Minute
|
||
|
if eventDelayS < shortDelayS {
|
||
|
return time.Duration(eventDelayS*shortDelayMultiplicator) * time.Second
|
||
|
}
|
||
|
if eventDelayS < longDelayS {
|
||
|
return longDelayTimeout
|
||
|
}
|
||
|
return time.Duration(eventDelayS) * time.Second
|
||
|
}
|