implemented priority preloading

This commit is contained in:
ston1th 2022-05-03 22:14:47 +02:00
commit eb6272e411
7 changed files with 134 additions and 107 deletions

View file

@ -46,7 +46,7 @@ func main() {
flag.StringVar(&listenHttp, "listen", "127.0.0.1:8080", "listen addr:port") flag.StringVar(&listenHttp, "listen", "127.0.0.1:8080", "listen addr:port")
flag.StringVar(&listenWebdav, "webdav", "", "listen addr:port for webdav") flag.StringVar(&listenWebdav, "webdav", "", "listen addr:port for webdav")
flag.StringVar(&listenCache, "cache", "", "listen addr:port for cache only") flag.StringVar(&listenCache, "cache", "", "listen addr:port for cache only")
flag.IntVar(&max, "max", -1, "max parallel preloads") flag.IntVar(&max, "max", 1, "max parallel preloads")
flag.IntVar(&bs, "bs", -1, "tune preload buffer size in bytes (default: 8192)") flag.IntVar(&bs, "bs", -1, "tune preload buffer size in bytes (default: 8192)")
flag.Int64Var(&quota, "quota", 1, "max disk usage quota for the dst cache in GiB") flag.Int64Var(&quota, "quota", 1, "max disk usage quota for the dst cache in GiB")
flag.BoolVar(&block, "block", true, "block until sftp is connected") flag.BoolVar(&block, "block", true, "block until sftp is connected")

View file

@ -50,13 +50,14 @@ func (p *preload) Read(data []byte) (n int, err error) {
func (f *File) Preload(ctx context.Context, unlock func()) { func (f *File) Preload(ctx context.Context, unlock func()) {
log := f.log log := f.log
defer f.Close() defer f.Close()
defer unlock()
if f.offline { if f.offline {
log.V(2).Error(errors.New("no preload in offline mode"), "error preloading file") log.V(2).Error(errors.New("no preload in offline mode"), "error preloading file")
unlock()
return return
} }
if f.md.FullyCached() { if f.md.FullyCached() {
log.V(2).Info("skipped preload for fully cached file") log.V(2).Info("skipped preload for fully cached file")
unlock()
return return
} }
@ -65,6 +66,7 @@ func (f *File) Preload(ctx context.Context, unlock func()) {
_, err := io.Copy(Discard, p) _, err := io.Copy(Discard, p)
if err == context.Canceled { if err == context.Canceled {
log.V(2).Info("preload canceled", "skipped", p.skipped, "written", p.written) log.V(2).Info("preload canceled", "skipped", p.skipped, "written", p.written)
// do not call unlock() to keep preload in list
return return
} }
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
@ -75,6 +77,7 @@ func (f *File) Preload(ctx context.Context, unlock func()) {
log.Error(err, "error closing cache file") log.Error(err, "error closing cache file")
} }
log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written) log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written)
unlock()
} }
func (f *File) Read(p []byte) (n int, err error) { func (f *File) Read(p []byte) (n int, err error) {

View file

@ -145,16 +145,16 @@ func (fs *FS) QuotaUsage() (cur, max int64) {
return fs.q.Usage() return fs.q.Usage()
} }
func (fs *FS) CancelPreload(name string) { func (fs *FS) RemovePreload(name string) {
fs.ph.CancelPreload(name) fs.ph.RemovePreload(name)
} }
func (fs *FS) Preloads() Preloads { func (fs *FS) Preloads() Preloads {
return fs.ph.Preloads() return fs.ph.Preloads()
} }
func (fs *FS) Preload(name string) { func (fs *FS) Preload(name string, prio int) {
fs.ph.Preload(name) fs.ph.Preload(name, prio)
} }
func skipLog(name string) bool { func skipLog(name string) bool {

View file

@ -4,138 +4,160 @@ package fs
import ( import (
"context" "context"
"errors"
"sync" "sync"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
type queue []*Preload
func (queue) LessReverse(i, j *Preload) bool { return i.Prio > j.Prio }
func (q *queue) Head() (p *Preload) {
l := len(*q)
if l > 0 {
p = (*q)[l-1]
}
return
}
func (q *queue) Pop() (p *Preload) {
l := len(*q)
if l > 0 {
p = q.Head()
*q = (*q)[:l-1]
}
return
}
func (q *queue) Sort() {
slices.SortFunc(*q, q.LessReverse)
}
func (q *queue) Add(ph *PreloadHandler, name string, prio int) {
for _, p := range *q {
if p.Name == name {
p.Prio += prio
q.Sort()
return
}
}
q.Append(&Preload{Name: name, ph: ph})
}
func (q *queue) Append(p *Preload) {
*q = append(*q, p)
q.Sort()
}
func (q *queue) Remove(name string) {
for i, p := range *q {
if p.Name == name {
p.stop()
*q = slices.Delete(*q, i, i+1)
q.Sort()
return
}
}
}
type Preload struct { type Preload struct {
ph *PreloadHandler
Name string Name string
cancel func() cancel func()
Prio int
Status int Status int
Running bool Running bool
} }
type Preloads []*Preload type Preloads []Preload
func (Preloads) Less(i, j *Preload) bool { return i.Name < j.Name }
type PreloadHandler struct { type PreloadHandler struct {
mu sync.RWMutex mu sync.RWMutex
log logr.Logger log logr.Logger
fs *FS fs *FS
pm map[string]*Preload q queue
sched chan *Preload fin chan string
fin chan string max int
pre int
max int
} }
func NewPreloadHandler(ctx context.Context, fs *FS, max int, log logr.Logger) (ph *PreloadHandler) { func NewPreloadHandler(ctx context.Context, fs *FS, max int, log logr.Logger) (ph *PreloadHandler) {
if max < -1 { if max <= 0 {
max = -1 max = 1
} }
ph = &PreloadHandler{ ph = &PreloadHandler{
log: log, log: log,
fs: fs, fs: fs,
pm: make(map[string]*Preload), q: make(queue, 0),
sched: make(chan *Preload, 1), fin: make(chan string, 1),
fin: make(chan string, 1), max: max,
max: max,
} }
go ph.preload(ctx)
go ph.preloadFinish(ctx) go ph.preloadFinish(ctx)
return return
} }
func (ph *PreloadHandler) CancelPreload(name string) { func (ph *PreloadHandler) RemovePreload(name string) {
ph.mu.Lock() ph.mu.RLock()
defer ph.mu.Unlock() defer ph.mu.RUnlock()
p, ok := ph.pm[name] ph.q.Remove(name)
if ok { ph.schedule()
if p.Running {
p.cancel()
}
delete(ph.pm, name)
}
} }
func (ph *PreloadHandler) Preloads() (ps Preloads) { func (ph *PreloadHandler) Preloads() (ps Preloads) {
ph.mu.RLock() ph.mu.RLock()
defer ph.mu.RUnlock() defer ph.mu.RUnlock()
for _, p := range ph.pm { ps = make(Preloads, len(ph.q))
ps = append(ps, &Preload{ for i, v := range ph.q {
Name: p.Name, ps[i] = *v
Running: p.Running, ps[i].Status = ph.fs.CacheStatus(v.Name)
Status: ph.fs.CacheStatus(p.Name),
})
} }
slices.SortFunc(ps, ps.Less)
return return
} }
func (ph *PreloadHandler) Preload(name string) { func (ph *PreloadHandler) Preload(name string, prio int) {
ph.mu.Lock() ph.mu.Lock()
defer ph.mu.Unlock() defer ph.mu.Unlock()
ph.q.Add(ph, name, prio)
ph.schedule()
}
p, ok := ph.pm[name] func (ph *PreloadHandler) schedule() {
if !ok { for i := 0; i < len(ph.q); i++ {
p = &Preload{Name: name} if i < ph.max {
ph.pm[name] = p ph.q[i].start()
if ph.max == -1 || ph.pre < ph.max { } else {
ph.sched <- p ph.q[i].stop()
} }
return
} }
}
func (p *Preload) start() {
if p.Running { if p.Running {
ph.log.WithValues("file", name).Error(errors.New("preload is running"), "error starting preload")
}
return
}
func (ph *PreloadHandler) nextPreload() {
if ph.pre == ph.max {
return return
} }
for _, p := range ph.pm { name := p.Name
if p.Running { file, err := p.ph.fs.Open(name)
continue if err != nil {
} p.ph.log.Error(err, "error staring next preload", "file", name)
ph.sched <- p
return return
} }
f, ok := file.(*File)
if !ok {
return
}
ctx, cancel := context.WithCancel(context.Background())
p.Running = true
p.cancel = cancel
go f.Preload(ctx, func() {
p.ph.fin <- name
})
} }
func (ph *PreloadHandler) preload(ctx context.Context) { func (p *Preload) stop() {
var p *Preload if p.Running {
for { p.cancel()
select { p.Running = false
case <-ctx.Done():
return
case p = <-ph.sched:
}
name := p.Name
file, err := ph.fs.Open(name)
if err != nil {
ph.log.Error(err, "error staring next preload", "file", name)
return
}
f, ok := file.(*File)
if !ok {
continue
}
ctx, cancel := context.WithCancel(context.Background())
ph.mu.Lock()
ph.pre++
p.Running = true
p.cancel = cancel
ph.mu.Unlock()
go f.Preload(ctx, func() {
ph.fin <- name
})
} }
} }
@ -147,23 +169,19 @@ func (ph *PreloadHandler) preloadFinish(ctx context.Context) {
return return
case name := <-ph.fin: case name := <-ph.fin:
ph.mu.Lock() ph.mu.Lock()
delete(ph.pm, name) ph.q.Remove(name)
ph.pre-- ph.schedule()
ph.nextPreload()
ph.mu.Unlock() ph.mu.Unlock()
} }
} }
} }
func (ph *PreloadHandler) close() { func (ph *PreloadHandler) close() {
for _, p := range ph.pm { for _, p := range ph.q {
if p.Running { p.stop()
p.cancel()
}
} }
for { for {
select { select {
case <-ph.sched:
case <-ph.fin: case <-ph.fin:
default: default:
return return

View file

@ -60,6 +60,7 @@ type file struct {
URI template.HTML URI template.HTML
Anchor string Anchor string
Status int Status int
Prio int
Running bool Running bool
} }
@ -136,12 +137,10 @@ func getPreloads(path string, fs *fs.FS) (dc dirContents) {
Name: template.HTML(p.Name), Name: template.HTML(p.Name),
URI: template.HTML(p.Name), URI: template.HTML(p.Name),
Status: p.Status, Status: p.Status,
Prio: p.Prio,
Running: p.Running, Running: p.Running,
}) })
} }
slices.SortFunc(dc.Files, func(i, _ file) bool {
return i.Running
})
return return
} }

View file

@ -85,11 +85,16 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if !d.IsDir() { if !d.IsDir() {
if option == "p" || option == "s" { if option == "p" || option == "i" || option == "d" || option == "s" {
if option == "p" { switch option {
fs.fs.Preload(p) case "p":
} else if option == "s" { fs.fs.Preload(p, 0)
fs.fs.CancelPreload(p) case "i":
fs.fs.Preload(p, 1)
case "d":
fs.fs.Preload(p, -1)
case "s":
fs.fs.RemovePreload(p)
} }
rdir := "/" rdir := "/"
if r.FormValue("r") == "preloads" { if r.FormValue("r") == "preloads" {

View file

@ -1,5 +1,7 @@
{{define "body" -}} {{define "body" -}}
<pre>[v]: show video <pre>[v]: show video
[+]: increase priority
[-]: decrease priority
[s]: stop preloading [s]: stop preloading
Quota: {{.QuotaCur}} / {{.QuotaMax}} GiB Quota: {{.QuotaCur}} / {{.QuotaMax}} GiB
</pre> </pre>
@ -12,7 +14,7 @@ Quota: {{.QuotaCur}} / {{.QuotaMax}} GiB
<tr class="listitem"> <tr class="listitem">
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td> <td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
<td class="listoptions"> <td class="listoptions">
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span class="status-{{if $s.Running}}green">[running{{else}}yellow">[queued{{end}}]</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.Name}}?o=s&r=preloads">[s]</a> {{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span class="status-{{if $s.Running}}green">[running{{else}}yellow">[queued{{end}}]</span><span>{{$s.Prio}}</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.Name}}?o=s&r=preloads">[s]</a><a href="{{$s.Name}}?o=i&r=preloads">[+]</a><a href="{{$s.Name}}?o=d&r=preloads">[-]</a>
</td> </td>
</tr> </tr>
<tr class="spacer"><td colspan="2"> <tr class="spacer"><td colspan="2">