Sybren A. Stüvel 0fc5ba0bc6 Manager: broadcast last-rendered image info via SocketIO
After processing an image in the "last-rendered" processor, a SocketIO
object is sent to clients to indicate the last-rendered image needs to
be (re)loaded.

This also moves the previously existing "done callback" from a single
function to a per-image callback, so that it can be called with the
right information in there, and only when that particular image is
actually done processing.

The notification message sent via SocketIO also contains the necessary
info to render the image, so that the web client doesn't have to call
the `fetchJobLastRenderedInfo` operation.
2022-06-30 18:36:24 +02:00

175 lines
4.6 KiB
Go

package last_rendered
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"context"
"errors"
"path/filepath"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
const (
// MaxImageSizeBytes is the maximum size in bytes allowed for to-be-processed images.
MaxImageSizeBytes int64 = 10 * 1024 * 1024
// queueSize determines how many images can be queued in memory before rejecting
// new requests to process.
queueSize = 3
thumbnailJPEGQuality = 80
)
var (
ErrQueueFull = errors.New("queue full")
// thumbnails specifies the thumbnail sizes. For efficiency, they should be
// listed from large to small, as each thumbnail is the input for the next
// one.
thumbnails = []Thumbspec{
{"last-rendered.jpg", 1920, 1080},
{"last-rendered-small.jpg", 600, 338},
{"last-rendered-tiny.jpg", 200, 112},
}
)
type Storage interface {
// ForJob returns the directory path for storing job-related files.
ForJob(jobUUID string) string
}
// LastRenderedProcessor processes "last-rendered" images and stores them with
// the job.
type LastRenderedProcessor struct {
storage Storage
// TODO: expand this queue to be per job, so that one spammy job doesn't block
// the queue for other jobs.
queue chan Payload
}
// Payload contains the actual image to process.
type Payload struct {
JobUUID string // Used to determine the directory to store the image.
WorkerUUID string // Just for logging.
MimeType string
Image []byte
// Callback is called when the image processing is finished.
Callback func()
}
// Thumbspec specifies a thumbnail size & filename.
type Thumbspec struct {
Filename string
MaxWidth int
MaxHeight int
}
func New(storage Storage) *LastRenderedProcessor {
return &LastRenderedProcessor{
storage: storage,
queue: make(chan Payload, queueSize),
}
}
// Run is the main loop for the processing of images. It will keep running until
// the context is closed.
func (lrp *LastRenderedProcessor) Run(ctx context.Context) {
log.Debug().Msg("last-rendered: queue runner running")
defer log.Debug().Msg("last-rendered: queue runner shutting down")
for {
select {
case <-ctx.Done():
return
case payload := <-lrp.queue:
lrp.processImage(payload)
}
}
}
// QueueImage queues an image for processing.
// Returns `ErrQueueFull` if there is no more space in the queue for new images.
func (lrp *LastRenderedProcessor) QueueImage(payload Payload) error {
logger := payload.sublogger(log.Logger)
select {
case lrp.queue <- payload:
logger.Debug().Msg("last-rendered: queued image for processing")
return nil
default:
logger.Debug().Msg("last-rendered: unable to queue image for processing")
return ErrQueueFull
}
}
// PathForJob returns the base path for this job's last-rendered images.
func (lrp *LastRenderedProcessor) PathForJob(jobUUID string) string {
return lrp.storage.ForJob(jobUUID)
}
// ThumbSpecs returns the thumbnail specifications.
func (lrp *LastRenderedProcessor) ThumbSpecs() []Thumbspec {
// Return a copy so modification of the returned slice won't affect the global
// `thumbnails` variable.
copied := make([]Thumbspec, len(thumbnails))
copy(copied, thumbnails)
return copied
}
// processImage down-scales the image to a few thumbnails for presentation in
// the web interface, and stores those in a job-specific directory.
//
// Because this is intended as internal queue-processing function, errors are
// logged but not returned.
func (lrp *LastRenderedProcessor) processImage(payload Payload) {
jobDir := lrp.PathForJob(payload.JobUUID)
logger := log.With().Str("jobDir", jobDir).Logger()
logger = payload.sublogger(logger)
// Decode the image.
image, err := decodeImage(payload)
if err != nil {
logger.Error().Err(err).Msg("last-rendered: unable to decode image")
return
}
// Generate the thumbnails.
for _, spec := range thumbnails {
thumbLogger := spec.sublogger(logger)
thumbLogger.Trace().Msg("last-rendered: creating thumbnail")
image = downscaleImage(spec, image)
imgpath := filepath.Join(jobDir, spec.Filename)
if err := saveJPEG(imgpath, image); err != nil {
thumbLogger.Error().Err(err).Msg("last-rendered: error saving thumbnail")
break
}
}
// Call the callback, if provided.
if payload.Callback != nil {
payload.Callback()
}
}
func (p Payload) sublogger(logger zerolog.Logger) zerolog.Logger {
return logger.With().
Str("job", p.JobUUID).
Str("producedByWorker", p.WorkerUUID).
Str("mime", p.MimeType).
Logger()
}
func (spec Thumbspec) sublogger(logger zerolog.Logger) zerolog.Logger {
return logger.With().
Int("width", spec.MaxWidth).
Int("height", spec.MaxHeight).
Str("filename", spec.Filename).
Logger()
}