638 lines
19 KiB
Go
638 lines
19 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/md5"
|
||
|
"crypto/sha256"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"html/template"
|
||
|
"image/png"
|
||
|
"io"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"os/signal"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
"unicode"
|
||
|
|
||
|
"deadbeef.codes/steven/cyclescaler"
|
||
|
"github.com/disintegration/imaging"
|
||
|
)
|
||
|
|
||
|
// Wip (work in progress) is instanciated for each frame / task that a slave is currently working on. They are destroyed when a slave is finished or if we've lost contact with the slave.
|
||
|
type Wip struct {
|
||
|
FrameNumber int
|
||
|
HeartbeatTimestamp time.Time
|
||
|
Output string
|
||
|
Timeout bool // Haven't received a hearbeat and slated for cleanup
|
||
|
CompletePercentage int
|
||
|
SlaveName string // name of the slave processing this frame
|
||
|
BadHealth bool // If we're missing heartbeats
|
||
|
}
|
||
|
|
||
|
// BatchJob is instanciated for each .blend file uploaded and contains data related to the total rendering that needs to happen
|
||
|
// The BatchJob contains the state of all the frames in the batch job. A frame can be in 3 different states. It can
|
||
|
// be sitting in RemainingFrames, it can be in a Wip, or it can be in CompletedFrames. When it moves to one state,
|
||
|
// it is removed from the previous one. If a job times out, the watchdog handler will throw it back into the remaining
|
||
|
// frames state
|
||
|
// We use 3 slices for tracking state instead of keeping the state inside of a "frame" object because it scales better when
|
||
|
// searching for frames in a specific state
|
||
|
type BatchJob struct {
|
||
|
Token string // the token for the blend file
|
||
|
BlendFileName string // original blend file name
|
||
|
|
||
|
//Job state
|
||
|
Started bool // whether the job is paused or not yet started
|
||
|
Completed bool // whether the job is completed or not
|
||
|
CompletePercentage int //
|
||
|
|
||
|
//Frame state
|
||
|
RemainingFrames []int // these are "undispatched" frames. If it's in progress, it's not in here, doesn't mean it's rendered.
|
||
|
Wips []Wip // work in progress, frames which have been scheduled to be rendered by a slave. Contains heartbeat timestamp
|
||
|
CompletedFrames []int // frames which have been completed
|
||
|
|
||
|
NumberOfFrames int // Always equal to len(RemainingFrames) + len(Wips) + len (CompletedFrames)
|
||
|
StartTime string
|
||
|
}
|
||
|
|
||
|
// HomePage is data that gets served to templates/home
|
||
|
type HomePage struct {
|
||
|
NotNoJobs bool // This is purposely a dumb name. If there are any previously or ongoing jobs... AKA this is only false if this master has just come online and nothing has ever been uploaded to it. If it's true, the home page will display the segment listing ongoing jobs.
|
||
|
Jobs *map[string]*BatchJob
|
||
|
Token string // This is the token that's used if a new batch job were to be uploaded
|
||
|
}
|
||
|
|
||
|
// JobStatusPageImages is instantiated for each image on the status page which has a Thumbnail
|
||
|
// as well as a RenderedFramePath. This struct maps the two together.
|
||
|
type JobStatusPageImages struct {
|
||
|
ThumbnailPath string
|
||
|
RenderedFramePath string
|
||
|
}
|
||
|
|
||
|
// JobStatusPageData that is served to templates/jobStatus.html
|
||
|
type JobStatusPage struct {
|
||
|
Images []JobStatusPageImages
|
||
|
UseTinyImages bool //If we have more than 100 images
|
||
|
Job *BatchJob
|
||
|
CompleteFrames string
|
||
|
LatestImage string
|
||
|
InProgress bool
|
||
|
}
|
||
|
|
||
|
// Data that is served to templates/invalidMime.html
|
||
|
type InvalidMimePage struct {
|
||
|
UploadedFileMime string
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
batchJobs map[string]*BatchJob // Uses session token as key
|
||
|
|
||
|
)
|
||
|
|
||
|
// http server setup and routing
|
||
|
func main() {
|
||
|
|
||
|
batchJobs = make(map[string]*BatchJob)
|
||
|
go watchdogTimer()
|
||
|
|
||
|
stop := make(chan os.Signal, 1)
|
||
|
signal.Notify(stop, os.Interrupt)
|
||
|
go func() {
|
||
|
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
|
||
|
http.HandleFunc("/", homePageHandler)
|
||
|
http.HandleFunc("/settings", settingsPageHandler)
|
||
|
http.HandleFunc("/jobStatus", jobStatusHandler)
|
||
|
http.HandleFunc("/getwork", getWorkHandler)
|
||
|
http.HandleFunc("/postwork", postWorkHandler)
|
||
|
http.HandleFunc("/heartbeat", heartbeatHandler)
|
||
|
http.HandleFunc("/404", notFoundHandler)
|
||
|
log.Fatal(http.ListenAndServe(":8097", nil))
|
||
|
}()
|
||
|
log.Println("started cyclescaler on 8097")
|
||
|
|
||
|
<-stop
|
||
|
fmt.Println("Shutting server down...")
|
||
|
}
|
||
|
|
||
|
// Collect the .blend file from the user
|
||
|
// Display list of running jobs
|
||
|
func homePageHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method == "GET" {
|
||
|
timeNow := time.Now().Unix()
|
||
|
h := md5.New()
|
||
|
io.WriteString(h, strconv.FormatInt(timeNow, 10))
|
||
|
|
||
|
homePage := HomePage{}
|
||
|
|
||
|
fmt.Println(len(batchJobs))
|
||
|
if len(batchJobs) > 0 {
|
||
|
homePage.NotNoJobs = true
|
||
|
homePage.Jobs = &batchJobs
|
||
|
}
|
||
|
|
||
|
homePage.Token = fmt.Sprintf("%x", h.Sum(nil))
|
||
|
|
||
|
t, err := template.ParseFiles("templates/home.html")
|
||
|
if err != nil {
|
||
|
log.Fatalf("could not parse home.html template file: %v", err)
|
||
|
}
|
||
|
err = t.Execute(w, homePage)
|
||
|
if err != nil {
|
||
|
log.Fatalf("could not execute home.html template file: %v", err)
|
||
|
}
|
||
|
|
||
|
} else {
|
||
|
err := r.ParseMultipartForm(32 << 22)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse form during file upload: %v", err)
|
||
|
return
|
||
|
}
|
||
|
token, ok := r.Form["token"]
|
||
|
if !ok {
|
||
|
log.Printf("couldn't GET token")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
file, handler, err := r.FormFile("uploadfile")
|
||
|
if err != nil {
|
||
|
fmt.Println(err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
defer file.Close()
|
||
|
if handler.Header.Get("Content-Type") != "application/x-blender" && handler.Header.Get("Content-Type") != "application/octet-stream" {
|
||
|
t, err := template.ParseFiles("templates/invalidMime.html")
|
||
|
if err != nil {
|
||
|
log.Fatalf("could not parse invalidMime.html template file: %v", err)
|
||
|
}
|
||
|
|
||
|
invalidMimePage := &InvalidMimePage{}
|
||
|
invalidMimePage.UploadedFileMime = handler.Header.Get("Content-Type")
|
||
|
|
||
|
t.Execute(w, invalidMimePage)
|
||
|
return
|
||
|
}
|
||
|
http.Redirect(w, r, fmt.Sprintf("/settings?token=%s", r.Form["token"][0]), http.StatusSeeOther)
|
||
|
|
||
|
// Store the uploaded file in the public directory and run it through a SHA256 hash function
|
||
|
f, err := os.OpenFile(fmt.Sprintf("public/blendfiles/%s.blend", token[0]), os.O_WRONLY|os.O_CREATE, 0666)
|
||
|
if err != nil {
|
||
|
log.Printf("could not create /public/blendfiles/%s.blend: %v", token[0], err)
|
||
|
return
|
||
|
}
|
||
|
defer f.Close()
|
||
|
hash := sha256.New()
|
||
|
multiWriter := io.MultiWriter(f, hash)
|
||
|
if _, err := io.Copy(multiWriter, file); err != nil {
|
||
|
log.Printf("could not copy file to multiWriter (file output and hash engine): %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Store the output of the hash function to a file in the public directory
|
||
|
hf, err := os.OpenFile(fmt.Sprintf("public/blendfiles/%s.sha256", token[0]), os.O_WRONLY|os.O_CREATE, 0666)
|
||
|
if err != nil {
|
||
|
log.Printf("coult not create /public/blendfiles/%s.sha256: %v", token[0], err)
|
||
|
return
|
||
|
}
|
||
|
hr := bytes.NewReader(hash.Sum(nil))
|
||
|
if _, err := io.Copy(hf, hr); err != nil {
|
||
|
log.Printf("could not copy hash of uploaded blend file to /public/blendfiles/%s.sha256: %v", token[0], err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
t := time.Now()
|
||
|
|
||
|
// Create the job and set known parameters
|
||
|
batchJob := BatchJob{
|
||
|
BlendFileName: handler.Filename,
|
||
|
Token: r.Form["token"][0],
|
||
|
StartTime: t.Format(time.RFC3339)}
|
||
|
batchJobs[r.Form["token"][0]] = &batchJob
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Collect settings from the user for the batch job
|
||
|
func settingsPageHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method == "GET" {
|
||
|
err := r.ParseForm()
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse form in settingsHandler GET: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, ok := r.Form["token"]
|
||
|
if !ok {
|
||
|
log.Println("couldn't get token in settingsHandler GET")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
t, err := template.ParseFiles("templates/settings.html")
|
||
|
if err != nil {
|
||
|
log.Fatalf("could not parse settings.html template file: %v", err)
|
||
|
}
|
||
|
t.Execute(w, batchJobs[r.Form["token"][0]])
|
||
|
|
||
|
} else {
|
||
|
// Begin render and redirect person to status page for that render
|
||
|
err := r.ParseMultipartForm(32 << 20)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse form in settingsHandler POST: %v", err)
|
||
|
return
|
||
|
}
|
||
|
token, ok := r.Form["token"]
|
||
|
if !ok {
|
||
|
log.Println("couldn't get token in settingsHandler POST")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var renderFrames string
|
||
|
_, ok = r.Form["renderframes"]
|
||
|
if ok {
|
||
|
renderFrames = stripSpaces(r.Form["renderframes"][0]) + "," //This is a hack to trigger the , case below to add the last set of numbers
|
||
|
}
|
||
|
|
||
|
log.Printf("renderFrames = %s", renderFrames)
|
||
|
|
||
|
//This is where we have to parse the input settings into job.RemainingFrames
|
||
|
var remainingFrames []int
|
||
|
var firstNumber int //Used for ranges of frames, such as 1-50 in the case matching below
|
||
|
firstNumber = -1
|
||
|
var currentNumber string
|
||
|
|
||
|
for _, char := range renderFrames {
|
||
|
switch char {
|
||
|
case ',':
|
||
|
frame, err := strconv.Atoi(currentNumber)
|
||
|
log.Printf("frame = %d", frame)
|
||
|
if err != nil {
|
||
|
log.Printf("error converting currentNumber '%s' to integer: %v", currentNumber, err)
|
||
|
}
|
||
|
if firstNumber == -1 {
|
||
|
remainingFrames = append(remainingFrames, frame)
|
||
|
} else {
|
||
|
for j := firstNumber; j < frame+1; j++ {
|
||
|
remainingFrames = append(remainingFrames, j)
|
||
|
}
|
||
|
}
|
||
|
firstNumber = -1
|
||
|
currentNumber = ""
|
||
|
|
||
|
case '-':
|
||
|
firstNumber, err = strconv.Atoi(currentNumber)
|
||
|
if err != nil {
|
||
|
log.Printf("error converting currentNumber '%s' to integer: %v", currentNumber, err)
|
||
|
}
|
||
|
currentNumber = ""
|
||
|
|
||
|
default:
|
||
|
currentNumber += string(char)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
batchJobs[token[0]].RemainingFrames = remainingFrames
|
||
|
batchJobs[token[0]].NumberOfFrames = len(remainingFrames)
|
||
|
batchJobs[token[0]].Started = true
|
||
|
|
||
|
renderedFramesDir := fmt.Sprintf("public/renderedframes/%s", token[0])
|
||
|
renderedThumbnailsDir := fmt.Sprintf("public/renderedframes-thumbnail/%s", token[0])
|
||
|
if _, err := os.Stat(renderedFramesDir); os.IsNotExist(err) {
|
||
|
os.Mkdir(renderedFramesDir, os.ModePerm)
|
||
|
}
|
||
|
if _, err := os.Stat(renderedThumbnailsDir); os.IsNotExist(err) {
|
||
|
os.Mkdir(renderedThumbnailsDir, os.ModePerm)
|
||
|
}
|
||
|
|
||
|
http.Redirect(w, r, fmt.Sprintf("/jobStatus?token=%s", r.Form["token"][0]), http.StatusSeeOther)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// The status page to review status for the render job, see frame thumbnails and renders in progress
|
||
|
func jobStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
err := r.ParseForm()
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse form in statusHandler: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, ok := r.Form["token"]
|
||
|
if !ok {
|
||
|
log.Println("couldn't get token in statusHandler")
|
||
|
return
|
||
|
}
|
||
|
token := r.Form["token"][0]
|
||
|
|
||
|
_, ok = batchJobs[token]
|
||
|
if !ok {
|
||
|
log.Println("request to see status page with token that doesn't exist")
|
||
|
http.Redirect(w, r, "/404", http.StatusSeeOther)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
t, err := template.ParseFiles("templates/jobStatus.html")
|
||
|
if err != nil {
|
||
|
log.Fatalf("could not parse jobStatus.html template file: %v", err)
|
||
|
}
|
||
|
|
||
|
jobStatusPage := JobStatusPage{}
|
||
|
jobStatusPage.Job = batchJobs[token]
|
||
|
|
||
|
f, err := os.Open(fmt.Sprintf("public/renderedframes-thumbnail/%s", token))
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
files, err := f.Readdir(-1)
|
||
|
f.Close()
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
for _, file := range files {
|
||
|
tnp := fmt.Sprintf("public/renderedframes-thumbnail/%s/%s", token, file.Name())
|
||
|
rfp := fmt.Sprintf("public/renderedframes/%s/%s", token, file.Name())
|
||
|
|
||
|
if strings.HasSuffix(tnp, ".png") {
|
||
|
spi := JobStatusPageImages{ThumbnailPath: tnp, RenderedFramePath: rfp}
|
||
|
jobStatusPage.Images = append(jobStatusPage.Images, spi)
|
||
|
}
|
||
|
}
|
||
|
sort.Slice(jobStatusPage.Images, func(i, j int) bool {
|
||
|
return jobStatusPage.Images[i].ThumbnailPath < jobStatusPage.Images[j].ThumbnailPath
|
||
|
})
|
||
|
|
||
|
if len(jobStatusPage.Images) > 0 {
|
||
|
jobStatusPage.LatestImage = jobStatusPage.Images[len(jobStatusPage.Images)-1].RenderedFramePath
|
||
|
} else {
|
||
|
jobStatusPage.LatestImage = "public/img/blankthumbnail.png"
|
||
|
}
|
||
|
|
||
|
if len(jobStatusPage.Images) > 500 {
|
||
|
jobStatusPage.UseTinyImages = true
|
||
|
}
|
||
|
|
||
|
if len(jobStatusPage.Job.Wips) > 0 {
|
||
|
jobStatusPage.InProgress = true
|
||
|
} else {
|
||
|
jobStatusPage.InProgress = false
|
||
|
}
|
||
|
|
||
|
t.Execute(w, jobStatusPage)
|
||
|
}
|
||
|
|
||
|
// A slave is requesting work - find them something to do
|
||
|
func getWorkHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
err := r.ParseForm()
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse form in getWorkHandler: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, ok := r.Form["slavename"]
|
||
|
if !ok {
|
||
|
log.Println("couldn't get slavename getWorkHandler")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
slaveName := r.Form["slavename"][0]
|
||
|
|
||
|
for _, job := range batchJobs {
|
||
|
if job.Completed || !job.Started { //Jobs in these states are skipped
|
||
|
continue
|
||
|
}
|
||
|
if len(job.RemainingFrames) > 0 {
|
||
|
slaveOrder := &cyclescaler.SlaveOrder{}
|
||
|
slaveOrder.Token = job.Token
|
||
|
|
||
|
slaveOrder.FrameNumber = job.RemainingFrames[0] // We grab the next frame to render
|
||
|
job.RemainingFrames = append(job.RemainingFrames[:0], job.RemainingFrames[1:]...) // Remove it from the remaining frames
|
||
|
|
||
|
wip := Wip{} //and then store it all in a WIP
|
||
|
wip.FrameNumber = slaveOrder.FrameNumber
|
||
|
wip.HeartbeatTimestamp = time.Now()
|
||
|
wip.Output = "Job dispatched to slave..."
|
||
|
wip.SlaveName = slaveName
|
||
|
job.Wips = append(job.Wips, wip)
|
||
|
|
||
|
if slaveOrder.Token != "" {
|
||
|
json, err := json.Marshal(slaveOrder)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to marshal job '%s' to json: %v", job.BlendFileName, err)
|
||
|
return
|
||
|
}
|
||
|
_, err = w.Write(json)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to write json to http response writer for job '%s': %v", job.BlendFileName, err)
|
||
|
return
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
if len(job.Wips) == 0 { //The job is now complete, but not yet marked as complete
|
||
|
job.Completed = true
|
||
|
}
|
||
|
}
|
||
|
w.Write([]byte("none")) // No work available, terminate the instance
|
||
|
}
|
||
|
|
||
|
// Accept a rendered png frame from a slave and save it to the public directory
|
||
|
func postWorkHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
fmt.Println("received post request to upload work")
|
||
|
err := r.ParseMultipartForm(32 << 20)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse multipart form in postWorkHandler")
|
||
|
return
|
||
|
}
|
||
|
token, ok := r.Form["token"]
|
||
|
if !ok {
|
||
|
log.Printf("couldn't get token for postWorkHandler")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, ok = r.Form["framenumber"]
|
||
|
if !ok {
|
||
|
log.Printf("couldn't get framenumber for postWorkHandler")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
frameNumber, err := strconv.Atoi(r.Form["framenumber"][0])
|
||
|
if err != nil {
|
||
|
log.Printf("failed to convert frame number '%s' to integer: %v", r.Form["framenumber"][0], err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
fmt.Printf("postWork framenumber '%d', token '%s'", frameNumber, token[0])
|
||
|
|
||
|
file, handler, err := r.FormFile("uploadfile")
|
||
|
if err != nil {
|
||
|
fmt.Println(err)
|
||
|
return
|
||
|
}
|
||
|
fmt.Println("1")
|
||
|
defer file.Close()
|
||
|
|
||
|
f, err := os.OpenFile(fmt.Sprintf("public/renderedframes/%s/%s", token[0], handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to create uploaded file locally: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
defer f.Close()
|
||
|
_, err = io.Copy(f, file)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to save local image file from uploaded render frame '%d', token '%s': %v", frameNumber, token[0], err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, ok = batchJobs[token[0]]
|
||
|
if !ok {
|
||
|
log.Printf("received a rendered frame '%d' for job with token '%s' but couldn't find this as an active job. master may have been restarted while render was in progress", frameNumber, token[0])
|
||
|
return
|
||
|
}
|
||
|
|
||
|
fmt.Println("finished saving uploaded file")
|
||
|
job := batchJobs[token[0]]
|
||
|
job.CompletedFrames = append(job.CompletedFrames, frameNumber)
|
||
|
job.CompletePercentage = (len(job.CompletedFrames) * 100) / job.NumberOfFrames
|
||
|
|
||
|
for i, wip := range job.Wips {
|
||
|
if wip.FrameNumber == frameNumber {
|
||
|
job.Wips = append(job.Wips[:i], job.Wips[i+1:]...) //Remove it from the Wips
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
f.Close()
|
||
|
|
||
|
src, err := imaging.Open(fmt.Sprintf("public/renderedframes/%s/%s", token[0], handler.Filename))
|
||
|
if err != nil {
|
||
|
log.Fatalf("failed to decode uploaded file during thumbnail creation: %v", err)
|
||
|
}
|
||
|
|
||
|
dstImageFit := imaging.Fit(src, 240, 135, imaging.Lanczos)
|
||
|
|
||
|
f, err = os.Create(fmt.Sprintf("public/renderedframes-thumbnail/%s/%s", token[0], handler.Filename))
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
defer f.Close()
|
||
|
png.Encode(f, dstImageFit)
|
||
|
|
||
|
}
|
||
|
|
||
|
// A slave is checking in to tell us he's still working hard. Also, provides an update on the current job.
|
||
|
func heartbeatHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
fmt.Println("received post request for heartbeat")
|
||
|
err := r.ParseForm()
|
||
|
if err != nil {
|
||
|
log.Printf("failed to parse multipart form in heartbeatHandler")
|
||
|
return
|
||
|
}
|
||
|
token, ok := r.Form["token"]
|
||
|
if !ok {
|
||
|
log.Printf("couldn't get token for heartbeatHandler")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
status, ok := r.Form["status"]
|
||
|
if !ok {
|
||
|
log.Printf("couldn't get status for heartbeatHandler")
|
||
|
}
|
||
|
|
||
|
_, ok = r.Form["framenumber"]
|
||
|
if !ok {
|
||
|
log.Printf("couldn't get framenumber for heartbeatHandler")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
frameNumber, err := strconv.Atoi(r.Form["framenumber"][0])
|
||
|
if err != nil {
|
||
|
log.Printf("failed to convert frame number '%s' to integer: %v", r.Form["framenumber"][0], err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, ok = batchJobs[token[0]]
|
||
|
if !ok {
|
||
|
log.Printf("received heartbeat for batch job with token '%s' but it doesn't exist", token[0])
|
||
|
return
|
||
|
}
|
||
|
|
||
|
job := batchJobs[token[0]]
|
||
|
var progress int
|
||
|
if strings.Contains(status[0], "View Layer Rendered") {
|
||
|
progString := strings.Split(status[0], "View Layer Rendered ")[1]
|
||
|
progString = strings.Split(progString, " Tiles")[0]
|
||
|
tiles := strings.Split(progString, "/")
|
||
|
currentTile, err := strconv.Atoi(tiles[0])
|
||
|
if err != nil {
|
||
|
log.Printf("couldn't convert current tile '%s' to integer: %v", tiles[0], err)
|
||
|
}
|
||
|
endTile, err := strconv.Atoi(tiles[1])
|
||
|
if err != nil {
|
||
|
log.Printf("couldn't convert endTile '%s' to integer: %v", tiles[1], err)
|
||
|
}
|
||
|
log.Printf("currenttile: %d\nendTile: %d", currentTile, endTile)
|
||
|
progress = (currentTile * 100) / endTile
|
||
|
}
|
||
|
|
||
|
ok = false
|
||
|
for i, wip := range job.Wips {
|
||
|
if wip.FrameNumber == frameNumber {
|
||
|
log.Printf("received heartbeat for batch job '%s' - current status '%s' - current progress '%d'", token[0], status[0], progress)
|
||
|
ok = true
|
||
|
job.Wips[i].HeartbeatTimestamp = time.Now()
|
||
|
job.Wips[i].Output = status[0]
|
||
|
job.Wips[i].CompletePercentage = progress
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if !ok {
|
||
|
log.Printf("received heartbeat for frame '%d' for batch job '%s' but the frame is not considered to be in progress?", frameNumber, token[0])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Checks all wips for all jobs for the heartbeat timestamp timeout, and considers the slave down if it's exceeded
|
||
|
// the work then becomes available to be redispatched to the next slave that requests work.
|
||
|
func watchdogTimer() {
|
||
|
for {
|
||
|
time.Sleep(time.Minute * 1) // We run this every minute
|
||
|
for _, job := range batchJobs {
|
||
|
for i, wip := range job.Wips {
|
||
|
if time.Since(wip.HeartbeatTimestamp) > time.Minute*1 {
|
||
|
job.Wips[i].BadHealth = true
|
||
|
} else {
|
||
|
job.Wips[i].BadHealth = false
|
||
|
}
|
||
|
if time.Since(wip.HeartbeatTimestamp) > time.Minute*5 {
|
||
|
log.Printf("slave for frame '%d on job '%s' hasn't checked in for over 5 minutes... considering them dead", wip.FrameNumber, job.Token)
|
||
|
job.RemainingFrames = append([]int{wip.FrameNumber}, job.RemainingFrames...) //Throw it to the front of the queue
|
||
|
job.Wips = append(job.Wips[:i], job.Wips[i+1:]...)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
t, err := template.ParseFiles("templates/404.html")
|
||
|
if err != nil {
|
||
|
log.Fatalf("could not parse 404.html template file - you know shit's going wrong when...: %v", err)
|
||
|
}
|
||
|
t.Execute(w, nil)
|
||
|
}
|
||
|
|
||
|
//A little helper function, removes all whitespace from a string
|
||
|
func stripSpaces(str string) string {
|
||
|
return strings.Map(func(r rune) rune {
|
||
|
if unicode.IsSpace(r) {
|
||
|
// if the character is a space, drop it
|
||
|
return -1
|
||
|
}
|
||
|
// else keep it in the string
|
||
|
return r
|
||
|
}, str)
|
||
|
}
|