diff --git a/internal/ignore/cache.go b/internal/ignore/cache.go
new file mode 100644
index 000000000..3667bcf45
--- /dev/null
+++ b/internal/ignore/cache.go
@@ -0,0 +1,95 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see .
+
+package ignore
+
+import (
+ "sync"
+ "time"
+)
+
+var (
+ caches = make(map[string]*cache)
+ cacheMut sync.Mutex
+)
+
+func init() {
+ // Periodically go through the cache and remove cache entries that have
+ // not been touched in the last two hours.
+ go cleanIgnoreCaches(2 * time.Hour)
+}
+
+type cache struct {
+ patterns []Pattern
+ entries map[string]cacheEntry
+ mut sync.Mutex
+}
+
+type cacheEntry struct {
+ value bool
+ access time.Time
+}
+
+func newCache(patterns []Pattern) *cache {
+ return &cache{
+ patterns: patterns,
+ entries: make(map[string]cacheEntry),
+ }
+}
+
+func (c *cache) clean(d time.Duration) {
+ c.mut.Lock()
+ for k, v := range c.entries {
+ if time.Since(v.access) > d {
+ delete(c.entries, k)
+ }
+ }
+ c.mut.Unlock()
+}
+
+func (c *cache) get(key string) (result, ok bool) {
+ c.mut.Lock()
+ res, ok := c.entries[key]
+ if ok {
+ res.access = time.Now()
+ c.entries[key] = res
+ }
+ c.mut.Unlock()
+ return res.value, ok
+}
+
+func (c *cache) set(key string, val bool) {
+ c.mut.Lock()
+ c.entries[key] = cacheEntry{val, time.Now()}
+ c.mut.Unlock()
+}
+
+func (c *cache) len() int {
+ c.mut.Lock()
+ l := len(c.entries)
+ c.mut.Unlock()
+ return l
+}
+
+func cleanIgnoreCaches(dur time.Duration) {
+ for {
+ time.Sleep(dur)
+ cacheMut.Lock()
+ for _, v := range caches {
+ v.clean(dur)
+ }
+ cacheMut.Unlock()
+ }
+}
diff --git a/internal/ignore/cache_test.go b/internal/ignore/cache_test.go
new file mode 100644
index 000000000..155e22c35
--- /dev/null
+++ b/internal/ignore/cache_test.go
@@ -0,0 +1,84 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see .
+
+package ignore
+
+import (
+ "testing"
+ "time"
+)
+
+func TestCache(t *testing.T) {
+ c := newCache(nil)
+
+ res, ok := c.get("nonexistent")
+ if res != false || ok != false {
+ t.Error("res %v, ok %v for nonexistent item", res, ok)
+ }
+
+ // Set and check some items
+
+ c.set("true", true)
+ c.set("false", false)
+
+ res, ok = c.get("true")
+ if res != true || ok != true {
+ t.Errorf("res %v, ok %v for true item", res, ok)
+ }
+
+ res, ok = c.get("false")
+ if res != false || ok != true {
+ t.Errorf("res %v, ok %v for false item", res, ok)
+ }
+
+ // Don't clean anything
+
+ c.clean(time.Second)
+
+ // Same values should exist
+
+ res, ok = c.get("true")
+ if res != true || ok != true {
+ t.Errorf("res %v, ok %v for true item", res, ok)
+ }
+
+ res, ok = c.get("false")
+ if res != false || ok != true {
+ t.Errorf("res %v, ok %v for false item", res, ok)
+ }
+
+ // Sleep and access, to get some data for clean
+
+ time.Sleep(100 * time.Millisecond)
+ c.get("true")
+ time.Sleep(100 * time.Millisecond)
+
+ // "false" was accessed 200 ms ago, "true" was accessed 100 ms ago.
+ // This should clean out "false" but not "true"
+
+ c.clean(150 * time.Millisecond)
+
+ // Same values should exist
+
+ _, ok = c.get("true")
+ if !ok {
+ t.Error("item should still exist")
+ }
+
+ _, ok = c.get("false")
+ if ok {
+ t.Errorf("item should have been cleaned")
+ }
+}
diff --git a/internal/ignore/ignore.go b/internal/ignore/ignore.go
index 16ce956e7..48d2ede75 100644
--- a/internal/ignore/ignore.go
+++ b/internal/ignore/ignore.go
@@ -28,24 +28,15 @@ import (
"github.com/syncthing/syncthing/internal/fnmatch"
)
-var caches = make(map[string]MatcherCache)
-
type Pattern struct {
match *regexp.Regexp
include bool
}
type Matcher struct {
- patterns []Pattern
- oldMatches map[string]bool
-
- newMatches map[string]bool
- mut sync.Mutex
-}
-
-type MatcherCache struct {
patterns []Pattern
- matches *map[string]bool
+ matches *cache
+ mut sync.Mutex
}
func Load(file string, cache bool) (*Matcher, error) {
@@ -55,6 +46,9 @@ func Load(file string, cache bool) (*Matcher, error) {
return matcher, err
}
+ cacheMut.Lock()
+ defer cacheMut.Unlock()
+
// Get the current cache object for the given file
cached, ok := caches[file]
if !ok || !patternsEqual(cached.patterns, matcher.patterns) {
@@ -62,12 +56,9 @@ func Load(file string, cache bool) (*Matcher, error) {
// store matches for the given set of patterns.
// Initialize oldMatches to indicate that we are interested in
// caching.
- matcher.oldMatches = make(map[string]bool)
- matcher.newMatches = make(map[string]bool)
- caches[file] = MatcherCache{
- patterns: matcher.patterns,
- matches: &matcher.newMatches,
- }
+ cached = newCache(matcher.patterns)
+ matcher.matches = cached
+ caches[file] = cached
return matcher, nil
}
@@ -75,10 +66,7 @@ func Load(file string, cache bool) (*Matcher, error) {
// matches map and update the pointer. (This prevents matches map from
// growing indefinately, as we only cache whatever we've matched in the last
// iteration, rather than through runtime history)
- matcher.oldMatches = *cached.matches
- matcher.newMatches = make(map[string]bool)
- cached.matches = &matcher.newMatches
- caches[file] = cached
+ matcher.matches = cached
return matcher, nil
}
@@ -94,27 +82,27 @@ func (m *Matcher) Match(file string) (result bool) {
return false
}
- // We have old matches map set, means we should do caching
- if m.oldMatches != nil {
- // Capture the result to the new matches regardless of who returns it
- defer func() {
- m.mut.Lock()
- m.newMatches[file] = result
- m.mut.Unlock()
- }()
- // Check perhaps we've seen this file before, and we already know
- // what the outcome is going to be.
- result, ok := m.oldMatches[file]
+ if m.matches != nil {
+ // Check the cache for a known result.
+ res, ok := m.matches.get(file)
if ok {
- return result
+ return res
}
+
+ // Update the cache with the result at return time
+ defer func() {
+ m.matches.set(file, result)
+ }()
}
+ // Check all the patterns for a match.
for _, pattern := range m.patterns {
if pattern.match.MatchString(file) {
return pattern.include
}
}
+
+ // Default to false.
return false
}
diff --git a/internal/ignore/ignore_test.go b/internal/ignore/ignore_test.go
index a4b0aadb1..dcf153080 100644
--- a/internal/ignore/ignore_test.go
+++ b/internal/ignore/ignore_test.go
@@ -173,12 +173,8 @@ func TestCaching(t *testing.T) {
t.Fatal(err)
}
- if pats.oldMatches == nil || len(pats.oldMatches) != 0 {
- t.Fatal("Expected empty map")
- }
-
- if pats.newMatches == nil || len(pats.newMatches) != 0 {
- t.Fatal("Expected empty map")
+ if pats.matches.len() != 0 {
+ t.Fatal("Expected empty cache")
}
if len(pats.patterns) != 4 {
@@ -191,7 +187,7 @@ func TestCaching(t *testing.T) {
pats.Match(letter)
}
- if len(pats.newMatches) != 4 {
+ if pats.matches.len() != 4 {
t.Fatal("Expected 4 cached results")
}
@@ -201,30 +197,10 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if len(pats.oldMatches) != 4 {
+ if pats.matches.len() != 4 {
t.Fatal("Expected 4 cached results")
}
- // Match less this time
-
- for _, letter := range []string{"b", "x", "y"} {
- pats.Match(letter)
- }
-
- if len(pats.newMatches) != 3 {
- t.Fatal("Expected 3 cached results")
- }
-
- // Reload file, expect the new outcomes to be provided
-
- pats, err = Load(fd1.Name(), true)
- if err != nil {
- t.Fatal(err)
- }
- if len(pats.oldMatches) != 3 {
- t.Fatal("Expected 3 cached results", len(pats.oldMatches))
- }
-
// Modify the include file, expect empty cache
fd2.WriteString("/z/\n")
@@ -234,7 +210,7 @@ func TestCaching(t *testing.T) {
t.Fatal(err)
}
- if len(pats.oldMatches) != 0 {
+ if pats.matches.len() != 0 {
t.Fatal("Expected 0 cached results")
}
@@ -250,7 +226,7 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if len(pats.oldMatches) != 3 {
+ if pats.matches.len() != 3 {
t.Fatal("Expected 3 cached results")
}
@@ -262,7 +238,7 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if len(pats.oldMatches) != 0 {
+ if pats.matches.len() != 0 {
t.Fatal("Expected cache invalidation")
}
@@ -278,7 +254,7 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if len(pats.oldMatches) != 3 {
+ if pats.matches.len() != 3 {
t.Fatal("Expected 3 cached results")
}
}