Merge pull request #1171 from syncthing/jobqueue

Add job queue (replaces #1060)
This commit is contained in:
Audrius Butkevicius committed 2015-01-02 15:18:50 +00:00
commit 1b066d6965
10 files changed
+456 -40

No files matched your search

+30 -12
View File
@@ -149,6 +149,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
postRestMux.HandleFunc("/rest/bump", withModel(m, restPostBump))
// A handler that splits requests between the two above and disables
// caching
@@ -314,19 +315,12 @@ func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var folder = qs.Get("folder")
files := m.NeedFolderFilesLimited(folder, 100) // max 100 files
progress, queued, rest := m.NeedFolderFiles(folder, 100)
// Convert the struct to a more loose structure, and inject the size.
output := make([]map[string]interface{}, 0, len(files))
for _, file := range files {
output = append(output, map[string]interface{}{
"Name": file.Name,
"Flags": file.Flags,
"Modified": file.Modified,
"Version": file.Version,
"LocalVersion": file.LocalVersion,
"NumBlocks": file.NumBlocks,
"Size": protocol.BlocksToSize(file.NumBlocks),
})
output := map[string][]map[string]interface{}{
"progress": toNeedSlice(progress),
"queued": toNeedSlice(queued),
"rest": toNeedSlice(rest),
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -650,6 +644,14 @@ func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
}
}
func restPostBump(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
file := qs.Get("file")
m.BringToFront(folder, file)
restGetNeed(m, w, r)
}
func getQR(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var text = qs.Get("text")
@@ -775,3 +777,19 @@ func mimeTypeForFile(file string) string {
return mime.TypeByExtension(ext)
}
}
func toNeedSlice(files []protocol.FileInfoTruncated) []map[string]interface{} {
output := make([]map[string]interface{}, len(files))
for i, file := range files {
output[i] = map[string]interface{}{
"Name": file.Name,
"Flags": file.Flags,
"Modified": file.Modified,
"Version": file.Version,
"LocalVersion": file.LocalVersion,
"NumBlocks": file.NumBlocks,
"Size": protocol.BlocksToSize(file.NumBlocks),
}
}
return output
}
+28 -12
View File
@@ -801,21 +801,37 @@
<hr/>
<table class="table table-striped table-condensed">
<tr ng-repeat="f in needed" ng-init="a = needAction(f)">
<tr ng-repeat="f in needed.progress" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.Name}}">{{f.Name | basename}}</td>
<td>
<span ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
<span class="show frontal">{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B</span>
</div>
</span>
<td ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
<span class="show frontal">
{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B
</span>
</div>
</td>
<td class="text-right small-data" ng-if="a != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.Name]">
<span ng-if="f.Size > 0">{{f.Size | binary}}B</span>
</td>
</tr>
<tr ng-repeat="f in needed.queued" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.Name}}">{{f.Name | basename}}</td>
<td class="text-right small-data">
<span ng-if="$index != 0" class="glyphicon glyphicon-chevron-up" ng-click="bumpFile(neededFolder, f.Name)"></span>
<span ng-if="f.Size > 0">{{f.Size | binary}}B</span>
</td>
</tr>
<tr ng-repeat="f in needed.rest" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.Name}}">{{f.Name | basename}}</td>
<td class="text-right small-data"><span ng-if="f.Size > 0">{{f.Size | binary}}B</span></td>
</tr>
</table>
@@ -1056,6 +1056,15 @@ angular.module('syncthing.core')
$http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder));
};
$scope.bumpFile = function (folder, file) {
$http.post(urlbase + "/bump?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file)).success(function (data) {
if ($scope.neededFolder == folder) {
console.log("bumpFile", folder, data);
$scope.needed = data;
}
});
};
// pseudo main. called on all definitions assigned
initController();
});
+2 -2
View File
@@ -147,7 +147,7 @@ func Assets() map[string][]byte {
bs, _ = ioutil.ReadAll(gr)
assets["assets/lang/valid-langs.js"] = bs
bs, _ = base64.StdEncoding.DecodeString("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 truncated
bs, _ = base64.StdEncoding.DecodeString("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 truncated
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
bs, _ = ioutil.ReadAll(gr)
assets["index.html"] = bs
@@ -167,7 +167,7 @@ func Assets() map[string][]byte {
bs, _ = ioutil.ReadAll(gr)
assets["scripts/syncthing/core/controllers/eventController.js"] = bs
bs, _ = base64.StdEncoding.DecodeString("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 truncated
bs, _ = base64.StdEncoding.DecodeString("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 truncated
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
bs, _ = ioutil.ReadAll(gr)
assets["scripts/syncthing/core/controllers/syncthingController.js"] = bs
+53 -11
View File
@@ -79,6 +79,8 @@ const (
type service interface {
Serve()
Stop()
Jobs() ([]string, []string) // In progress, Queued
BringToFront(string)
}
type Model struct {
@@ -189,6 +191,7 @@ func (m *Model) StartFolderRW(folder string) {
copiers: cfg.Copiers,
pullers: cfg.Pullers,
finishers: cfg.Finishers,
queue: newJobQueue(),
}
m.folderRunners[folder] = p
m.fmut.Unlock()
@@ -416,22 +419,50 @@ func (m *Model) NeedSize(folder string) (files int, bytes int64) {
return
}
// NeedFiles returns the list of currently needed files, stopping at maxFiles
// files. Limit <= 0 is ignored.
func (m *Model) NeedFolderFilesLimited(folder string, maxFiles int) []protocol.FileInfoTruncated {
// NeedFiles returns the list of currently needed files in progress, queued,
// and to be queued on next puller iteration. Also takes a soft cap which is
// only respected when adding files from the model rather than the runner queue.
func (m *Model) NeedFolderFiles(folder string, max int) ([]protocol.FileInfoTruncated, []protocol.FileInfoTruncated, []protocol.FileInfoTruncated) {
defer m.leveldbPanicWorkaround()
m.fmut.RLock()
defer m.fmut.RUnlock()
if rf, ok := m.folderFiles[folder]; ok {
fs := make([]protocol.FileInfoTruncated, 0, maxFiles)
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
fs = append(fs, f.(protocol.FileInfoTruncated))
return maxFiles <= 0 || len(fs) < maxFiles
})
return fs
var progress, queued, rest []protocol.FileInfoTruncated
var seen map[string]bool
runner, ok := m.folderRunners[folder]
if ok {
progressNames, queuedNames := runner.Jobs()
progress = make([]protocol.FileInfoTruncated, len(progressNames))
queued = make([]protocol.FileInfoTruncated, len(queuedNames))
seen = make(map[string]bool, len(progressNames)+len(queuedNames))
for i, name := range progressNames {
progress[i] = rf.GetGlobal(name).ToTruncated() /// XXX: Should implement GetGlobalTruncated directly
seen[name] = true
}
for i, name := range queuedNames {
queued[i] = rf.GetGlobal(name).ToTruncated() /// XXX: Should implement GetGlobalTruncated directly
seen[name] = true
}
}
left := max - len(progress) - len(queued)
if max < 1 || left > 0 {
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
left--
ft := f.(protocol.FileInfoTruncated)
if !seen[ft.Name] {
rest = append(rest, ft)
}
return max < 1 || left > 0
})
}
return progress, queued, rest
}
return nil
return nil, nil, nil
}
// Index is called when a new device is connected and we receive their full index.
@@ -1336,7 +1367,7 @@ func (m *Model) RemoteLocalVersion(folder string) uint64 {
return ver
}
func (m *Model) availability(folder string, file string) []protocol.DeviceID {
func (m *Model) availability(folder, file string) []protocol.DeviceID {
// Acquire this lock first, as the value returned from foldersFiles can
// gen heavily modified on Close()
m.pmut.RLock()
@@ -1359,6 +1390,17 @@ func (m *Model) availability(folder string, file string) []protocol.DeviceID {
return availableDevices
}
// Bump the given files priority in the job queue
func (m *Model) BringToFront(folder, file string) {
m.pmut.RLock()
defer m.pmut.RUnlock()
runner, ok := m.folderRunners[folder]
if ok {
runner.BringToFront(file)
}
}
func (m *Model) String() string {
return fmt.Sprintf("model@%p", m)
}
+23 -3
View File
@@ -78,6 +78,7 @@ type Puller struct {
copiers int
pullers int
finishers int
queue *jobQueue
}
// Serve will run scans and pulls. It will return when Stop()ed or on a
@@ -337,15 +338,23 @@ func (p *Puller) pullerIteration(checksum bool, ignores *ignore.Matcher) int {
p.handleDir(file)
default:
// A new or changed file or symlink. This is the only case where we
// do stuff in the background; the other three are done
// synchronously.
p.handleFile(file, copyChan, finisherChan)
// do stuff concurrently in the background
p.queue.Push(file.Name)
}
changed++
return true
})
for {
fileName, ok := p.queue.Pop()
if !ok {
break
}
f := p.model.CurrentGlobalFile(p.folder, fileName)
p.handleFile(f, copyChan, finisherChan)
}
// Signal copy and puller routines that we are done with the in data for
// this iteration. Wait for them to finish.
close(copyChan)
@@ -483,6 +492,7 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt
if debug {
l.Debugln(p, "taking shortcut on", file.Name)
}
p.queue.Done(file.Name)
if file.IsSymlink() {
p.shortcutSymlink(curFile, file)
} else {
@@ -850,6 +860,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
continue
}
p.queue.Done(state.file.Name)
p.performFinish(state)
p.model.receivedFile(p.folder, state.file.Name)
if p.progressEmitter != nil {
@@ -859,6 +870,15 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
}
}
// Moves the given filename to the front of the job queue
func (p *Puller) BringToFront(filename string) {
p.queue.BringToFront(filename)
}
func (p *Puller) Jobs() ([]string, []string) {
return p.queue.Jobs()
}
func invalidateFolder(cfg *config.Configuration, folderID string, err error) {
for i := range cfg.Folders {
folder := &cfg.Folders[i]
+94
View File
@@ -0,0 +1,94 @@
// 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 <http://www.gnu.org/licenses/>.
package model
import "sync"
type jobQueue struct {
progress []string
queued []string
mut sync.Mutex
}
func newJobQueue() *jobQueue {
return &jobQueue{}
}
func (q *jobQueue) Push(file string) {
q.mut.Lock()
q.queued = append(q.queued, file)
q.mut.Unlock()
}
func (q *jobQueue) Pop() (string, bool) {
q.mut.Lock()
defer q.mut.Unlock()
if len(q.queued) == 0 {
return "", false
}
var f string
f = q.queued[0]
q.queued = q.queued[1:]
q.progress = append(q.progress, f)
return f, true
}
func (q *jobQueue) BringToFront(filename string) {
q.mut.Lock()
defer q.mut.Unlock()
for i, cur := range q.queued {
if cur == filename {
if i > 0 {
// Shift the elements before the selected element one step to
// the right, overwriting the selected element
copy(q.queued[1:i+1], q.queued[0:])
// Put the selected element at the front
q.queued[0] = cur
}
return
}
}
}
func (q *jobQueue) Done(file string) {
q.mut.Lock()
defer q.mut.Unlock()
for i := range q.progress {
if q.progress[i] == file {
copy(q.progress[i:], q.progress[i+1:])
q.progress = q.progress[:len(q.progress)-1]
return
}
}
}
func (q *jobQueue) Jobs() ([]string, []string) {
q.mut.Lock()
defer q.mut.Unlock()
progress := make([]string, len(q.progress))
copy(progress, q.progress)
queued := make([]string, len(q.queued))
copy(queued, q.queued)
return progress, queued
}
+200
View File
@@ -0,0 +1,200 @@
// 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 <http://www.gnu.org/licenses/>.
package model
import (
"fmt"
"reflect"
"testing"
)
func TestJobQueue(t *testing.T) {
// Some random actions
q := newJobQueue()
q.Push("f1")
q.Push("f2")
q.Push("f3")
q.Push("f4")
progress, queued := q.Jobs()
if len(progress) != 0 || len(queued) != 4 {
t.Fatal("Wrong length")
}
for i := 1; i < 5; i++ {
n, ok := q.Pop()
if !ok || n != fmt.Sprintf("f%d", i) {
t.Fatal("Wrong element")
}
progress, queued = q.Jobs()
if len(progress) != 1 || len(queued) != 3 {
t.Log(progress)
t.Log(queued)
t.Fatal("Wrong length")
}
q.Done(n)
progress, queued = q.Jobs()
if len(progress) != 0 || len(queued) != 3 {
t.Fatal("Wrong length", len(progress), len(queued))
}
q.Push(n)
progress, queued = q.Jobs()
if len(progress) != 0 || len(queued) != 4 {
t.Fatal("Wrong length")
}
q.Done("f5") // Does not exist
progress, queued = q.Jobs()
if len(progress) != 0 || len(queued) != 4 {
t.Fatal("Wrong length")
}
}
if len(q.progress) > 0 || len(q.queued) != 4 {
t.Fatal("Wrong length")
}
for i := 4; i > 0; i-- {
progress, queued = q.Jobs()
if len(progress) != 4-i || len(queued) != i {
t.Fatal("Wrong length")
}
s := fmt.Sprintf("f%d", i)
q.BringToFront(s)
progress, queued = q.Jobs()
if len(progress) != 4-i || len(queued) != i {
t.Fatal("Wrong length")
}
n, ok := q.Pop()
if !ok || n != s {
t.Fatal("Wrong element")
}
progress, queued = q.Jobs()
if len(progress) != 5-i || len(queued) != i-1 {
t.Fatal("Wrong length")
}
q.Done("f5") // Does not exist
progress, queued = q.Jobs()
if len(progress) != 5-i || len(queued) != i-1 {
t.Fatal("Wrong length")
}
}
_, ok := q.Pop()
if len(q.progress) != 4 || ok {
t.Fatal("Wrong length")
}
q.Done("f1")
q.Done("f2")
q.Done("f3")
q.Done("f4")
q.Done("f5") // Does not exist
_, ok = q.Pop()
if len(q.progress) != 0 || ok {
t.Fatal("Wrong length")
}
progress, queued = q.Jobs()
if len(progress) != 0 || len(queued) != 0 {
t.Fatal("Wrong length")
}
q.BringToFront("")
q.Done("f5") // Does not exist
progress, queued = q.Jobs()
if len(progress) != 0 || len(queued) != 0 {
t.Fatal("Wrong length")
}
}
func TestBringToFront(t *testing.T) {
q := newJobQueue()
q.Push("f1")
q.Push("f2")
q.Push("f3")
q.Push("f4")
_, queued := q.Jobs()
if !reflect.DeepEqual(queued, []string{"f1", "f2", "f3", "f4"}) {
t.Errorf("Incorrect order %v at start", queued)
}
q.BringToFront("f1") // corner case: does nothing
_, queued = q.Jobs()
if !reflect.DeepEqual(queued, []string{"f1", "f2", "f3", "f4"}) {
t.Errorf("Incorrect order %v", queued)
}
q.BringToFront("f3")
_, queued = q.Jobs()
if !reflect.DeepEqual(queued, []string{"f3", "f1", "f2", "f4"}) {
t.Errorf("Incorrect order %v", queued)
}
q.BringToFront("f2")
_, queued = q.Jobs()
if !reflect.DeepEqual(queued, []string{"f2", "f3", "f1", "f4"}) {
t.Errorf("Incorrect order %v", queued)
}
q.BringToFront("f4") // corner case: last element
_, queued = q.Jobs()
if !reflect.DeepEqual(queued, []string{"f4", "f2", "f3", "f1"}) {
t.Errorf("Incorrect order %v", queued)
}
}
func BenchmarkJobQueueBump(b *testing.B) {
files := genFiles(b.N)
q := newJobQueue()
for _, f := range files {
q.Push(f.Name)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
q.BringToFront(files[i].Name)
}
}
func BenchmarkJobQueuePushPopDone10k(b *testing.B) {
files := genFiles(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
q := newJobQueue()
for _, f := range files {
q.Push(f.Name)
}
for range files {
n, _ := q.Pop()
q.Done(n)
}
}
}
+6
View File
@@ -75,3 +75,9 @@ func (s *Scanner) Stop() {
func (s *Scanner) String() string {
return fmt.Sprintf("scanner/%s@%p", s.folder, s)
}
func (s *Scanner) BringToFront(string) {}
func (s *Scanner) Jobs() ([]string, []string) {
return nil, nil
}
+11
View File
@@ -69,6 +69,17 @@ func (f FileInfo) HasPermissionBits() bool {
return f.Flags&FlagNoPermBits == 0
}
func (f FileInfo) ToTruncated() FileInfoTruncated {
return FileInfoTruncated{
Name: f.Name,
Flags: f.Flags,
Modified: f.Modified,
Version: f.Version,
LocalVersion: f.LocalVersion,
NumBlocks: uint32(len(f.Blocks)),
}
}
// Used for unmarshalling a FileInfo structure but skipping the actual block list
type FileInfoTruncated struct {
Name string // max:8192