process load balancing: broken but in progress

This commit is contained in:
Julien Neuhart
2019-09-19 17:28:29 +02:00
parent c5429f7efa
commit 171f93662f
33 changed files with 1681 additions and 117 deletions

View File

@@ -0,0 +1 @@
package prinery

View File

@@ -0,0 +1,123 @@
package prinery
import (
"context"
"fmt"
"github.com/thecodingmachine/gotenberg/internal/pkg/print"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
type request struct {
ctx context.Context
logger xlog.Logger
print print.Print
dest string
result chan error
}
type worker struct {
work chan request
proc process.Process
}
func (w *worker) do(done chan *worker) {
for {
req := <-w.work
req.result <- req.print.Print(req.ctx, req.dest, w.proc)
done <- w
}
}
type Prinery struct {
logger xlog.Logger
manager process.Manager
work chan request
pool chan *worker
done chan *worker
}
func New(logger xlog.Logger, manager process.Manager, key process.Key) (*Prinery, error) {
const op string = "prinery.New"
processes := manager.Processes(key)
nWorkers := len(processes)
if nWorkers == 0 {
err := fmt.Errorf("no processes found for key '%s'", string(key))
return nil, xerror.New(op, err)
}
logger.DebugfOp(op, "found '%d' processes for key '%s'", nWorkers, string(key))
work := make(chan request, 1)
pool := make(chan *worker, nWorkers)
done := make(chan *worker, nWorkers)
for _, p := range processes {
w := &worker{
work: work,
proc: p,
}
pool <- w
go w.do(done)
}
return &Prinery{
logger: logger,
manager: manager,
work: work,
pool: pool,
done: done,
}, nil
}
func (p *Prinery) PrintRequest(ctx context.Context, logger xlog.Logger, prnt print.Print, dest string) error {
const op string = "prinery.Prinery.PrintRequest"
req := request{
ctx: ctx,
logger: logger,
print: prnt,
dest: dest,
result: make(chan error),
}
p.dispatch(req)
err := <-req.result
if err != nil {
return xerror.New(op, err)
}
return nil
}
func (p *Prinery) Start() {
for {
select {
case req := <-p.work:
p.dispatch(req)
case w := <-p.done:
p.completed(w)
}
}
}
func (p *Prinery) dispatch(req request) {
const op string = "prinery.Prinery.dispatch"
select {
case w := <-p.pool:
w.work <- req
case <-req.ctx.Done():
req.result <- xerror.New(op, req.ctx.Err())
}
}
func (p *Prinery) completed(w *worker) {
const op string = "prinery.Prinerty.completed"
go func() {
// check process viability.
isViable := p.manager.IsViable(w.proc)
// check process memory usage.
// TODO handle error.
memory, _ := p.manager.Memory(w.proc)
p.logger.DebugfOp(op, "%s: isViable = %t, memory = %d", w.proc.ID(), isViable, memory)
// TODO manage viability and memory usage.
// pushing back the worker.
p.pool <- w
}()
}

View File

@@ -0,0 +1,265 @@
package print
import (
"context"
"fmt"
"io/ioutil"
"time"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/devtool"
"github.com/mafredri/cdp/protocol/network"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/protocol/target"
"github.com/mafredri/cdp/rpcc"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerrgroup"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
type chromePrint struct {
logger xlog.Logger
url string
opts ChromePrintOptions
}
// ChromePrintOptions helps customizing the
// Google Chrome Print result.
type ChromePrintOptions struct {
WaitDelay float64
HeaderHTML string
FooterHTML string
PaperWidth float64
PaperHeight float64
MarginTop float64
MarginBottom float64
MarginLeft float64
MarginRight float64
Landscape bool
}
// DefaultChromePrintOptions returns the default
// Google Chrome Print options.
func DefaultChromePrintOptions() ChromePrintOptions {
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
return ChromePrintOptions{
WaitDelay: 0.0,
HeaderHTML: defaultHeaderFooterHTML,
FooterHTML: defaultHeaderFooterHTML,
PaperWidth: 8.27,
PaperHeight: 11.7,
MarginTop: 1.0,
MarginBottom: 1.0,
MarginLeft: 1.0,
MarginRight: 1.0,
Landscape: false,
}
}
func (p chromePrint) Print(ctx context.Context, dest string, proc process.Process) error {
const op string = "print.chromePrint.Print"
resolver := func() error {
devtEndpoint := fmt.Sprintf("http://%s:%d", proc.Host(), proc.Port())
devt, err := devtool.New(devtEndpoint).Version(ctx)
if err != nil {
return err
}
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
if err != nil {
return err
}
defer devtConn.Close() // nolint: errcheck
// create a new CDP Client that uses conn.
devtClient := cdp.NewClient(devtConn)
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
if err != nil {
return err
}
/*
close the browser context when done.
we're not using the "default" context
as it may timeout before actually closing
the browser context.
see: https://github.com/mafredri/cdp/issues/101#issuecomment-524533670
*/
disposeBrowserContextArgs := target.NewDisposeBrowserContextArgs(newContextTarget.BrowserContextID)
defer devtClient.Target.DisposeBrowserContext(context.Background(), disposeBrowserContextArgs) // nolint: errcheck
// create a new blank target with the new browser context.
createTargetArgs := target.
NewCreateTargetArgs("about:blank").
SetBrowserContextID(newContextTarget.BrowserContextID)
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
if err != nil {
return err
}
// connect the client to the new target.
newTargetWsURL := fmt.Sprintf("ws://%s:%d/devtools/page/%s", proc.Host(), proc.Port(), newTarget.TargetID)
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
if err != nil {
return err
}
defer newContextConn.Close() // nolint: errcheck
// create a new CDP Client that uses newContextConn.
targetClient := cdp.NewClient(newContextConn)
/*
close the target when done.
we're not using the "default" context
as it may timeout before actually closing
the target.
see: https://github.com/mafredri/cdp/issues/101#issuecomment-524533670
*/
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
defer targetClient.Target.CloseTarget(context.Background(), closeTargetArgs) // nolint: errcheck
// enable all events.
if err := p.enableEvents(ctx, targetClient); err != nil {
return err
}
// listen for all events.
if err := p.listenEvents(ctx, targetClient); err != nil {
return err
}
// apply a wait delay (if any).
if p.opts.WaitDelay > 0.0 {
// wait for a given amount of time (useful for javascript delay).
p.logger.DebugfOp(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay)
time.Sleep(xtime.Duration(p.opts.WaitDelay))
} else {
p.logger.DebugOp(op, "no wait delay to apply, moving on...")
}
// print the page to PDF.
print, err := targetClient.Page.PrintToPDF(
ctx,
page.NewPrintToPDFArgs().
SetPaperWidth(p.opts.PaperWidth).
SetPaperHeight(p.opts.PaperHeight).
SetMarginTop(p.opts.MarginTop).
SetMarginBottom(p.opts.MarginBottom).
SetMarginLeft(p.opts.MarginLeft).
SetMarginRight(p.opts.MarginRight).
SetLandscape(p.opts.Landscape).
SetDisplayHeaderFooter(true).
SetHeaderTemplate(p.opts.HeaderHTML).
SetFooterTemplate(p.opts.FooterHTML).
SetPrintBackground(true),
)
if err != nil {
return err
}
if err := ioutil.WriteFile(dest, print.Data, 0644); err != nil {
return err
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p chromePrint) enableEvents(ctx context.Context, client *cdp.Client) error {
const op string = "print.chromePrint.enableEvents"
// enable all the domain events that we're interested in.
if err := xerrgroup.Run(
func() error { return client.DOM.Enable(ctx) },
func() error { return client.Network.Enable(ctx, network.NewEnableArgs()) },
func() error { return client.Page.Enable(ctx) },
func() error {
return client.Page.SetLifecycleEventsEnabled(ctx, page.NewSetLifecycleEventsEnabledArgs(true))
},
func() error { return client.Runtime.Enable(ctx) },
); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p chromePrint) listenEvents(ctx context.Context, client *cdp.Client) error {
const op string = "print.chromePrint.listenEvents"
resolver := func() error {
// make sure Page events are enabled.
if err := client.Page.Enable(ctx); err != nil {
return err
}
// make sure Network events are enabled.
if err := client.Network.Enable(ctx, nil); err != nil {
return err
}
// create all clients for events.
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
if err != nil {
return err
}
defer domContentEventFired.Close() // nolint: errcheck
loadEventFired, err := client.Page.LoadEventFired(ctx)
if err != nil {
return err
}
defer loadEventFired.Close() // nolint: errcheck
lifecycleEvent, err := client.Page.LifecycleEvent(ctx)
if err != nil {
return err
}
defer lifecycleEvent.Close() // nolint: errcheck
loadingFinished, err := client.Network.LoadingFinished(ctx)
if err != nil {
return err
}
defer loadingFinished.Close() // nolint: errcheck
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
return err
}
// wait for all events.
return xerrgroup.Run(
func() error {
_, err := domContentEventFired.Recv()
if err != nil {
return err
}
p.logger.DebugOp(op, "event 'domContentEventFired' received")
return nil
},
func() error {
_, err := loadEventFired.Recv()
if err != nil {
return err
}
p.logger.DebugOp(op, "event 'loadEventFired' received")
return nil
},
func() error {
const networkIdleEventName string = "networkIdle"
for {
ev, err := lifecycleEvent.Recv()
if err != nil {
return err
}
p.logger.DebugfOp(op, "event '%s' received", ev.Name)
if ev.Name == networkIdleEventName {
break
}
}
return nil
},
func() error {
_, err := loadingFinished.Recv()
if err != nil {
return err
}
p.logger.DebugOp(op, "event 'loadingFinished' received")
return nil
},
)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Print(new(chromePrint))
)

View File

@@ -0,0 +1 @@
package print

View File

@@ -0,0 +1,18 @@
package print
import (
"fmt"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// NewHTMLPrint returns a Print for
// converting an HTML file to PDF.
func NewHTMLPrint(logger xlog.Logger, fpath string, opts ChromePrintOptions) Print {
URL := fmt.Sprintf("file://%s", fpath)
return chromePrint{
logger: logger,
url: URL,
opts: opts,
}
}

View File

@@ -0,0 +1,70 @@
package print
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"path/filepath"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
// NewMarkdownPrint returns a Print for
// converting a Markdown file to PDF.
func NewMarkdownPrint(logger xlog.Logger, fpath string, opts ChromePrintOptions) (Print, error) {
const op string = "print.NewMarkdownPrint"
resolver := func() (string, error) {
tmpl, err := template.
New(filepath.Base(fpath)).
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
ParseFiles(fpath)
if err != nil {
return "", err
}
dirPath := filepath.Dir(fpath)
data := &templateData{DirPath: dirPath}
logger.DebugOp(op, "converting Markdown files to HTML...")
var buffer bytes.Buffer
if err := tmpl.Execute(&buffer, data); err != nil {
return "", err
}
baseFilename := xrand.Get()
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
logger.DebugOp(op, "writing the HTML from previous conversion(s) into new file...")
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
return "", err
}
return fmt.Sprintf("file://%s", dst), nil
}
URL, err := resolver()
if err != nil {
return chromePrint{}, xerror.New(op, err)
}
return chromePrint{
logger: logger,
url: URL,
opts: opts,
}, nil
}
type templateData struct {
DirPath string
}
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
const op string = "print.markdownToHTML"
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
b, err := ioutil.ReadFile(fpath)
if err != nil {
return "", xerror.New(op, err)
}
unsafe := blackfriday.Run(b)
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
/* #nosec */
return template.HTML(content), nil
}

View File

@@ -0,0 +1,44 @@
package print
import (
"context"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
type mergePrint struct {
logger xlog.Logger
fpaths []string
}
// NewMergePrint returns a Print for
// merging PDF files.
func NewMergePrint(logger xlog.Logger, fpaths []string) Print {
return mergePrint{
logger: logger,
fpaths: fpaths,
}
}
func (p mergePrint) Print(ctx context.Context, dest string, proc process.Process) error {
const op string = "print.mergePrint.Print"
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
resolver := func() error {
var args []string
args = append(args, p.fpaths...)
args = append(args, "cat", "output", dest)
cmd, err := xexec.CommandContext(ctx, p.logger, "pdftk", args...)
if err != nil {
return err
}
xexec.LogBeforeExecute(p.logger, cmd)
return cmd.Run()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}

View File

@@ -0,0 +1,109 @@
package print
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
type officePrint struct {
logger xlog.Logger
fpaths []string
opts OfficePrintOptions
}
// OfficePrintOptions helps customizing the
// LibreOffice Print result.
type OfficePrintOptions struct {
Landscape bool
}
// DefaultOfficePrinterOptions returns the default
// LibreOffice Print options.
func DefaultOfficePrinterOptions() OfficePrintOptions {
return OfficePrintOptions{
Landscape: false,
}
}
// NewOfficePrint returns a Print for
// converting Office documents to PDF.
func NewOfficePrint(logger xlog.Logger, fpaths []string, opts OfficePrintOptions) Print {
return officePrint{
logger: logger,
fpaths: fpaths,
opts: opts,
}
}
func (p officePrint) Print(ctx context.Context, dest string, proc process.Process) error {
const op string = "print.officePrint.Print"
resolver := func() error {
fpaths := make([]string, len(p.fpaths))
dirPath := filepath.Dir(dest)
for i, fpath := range p.fpaths {
baseFilename := xrand.Get()
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil {
return err
}
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
fpaths[i] = tmpDest
}
if len(fpaths) == 1 {
p.logger.DebugOp(op, "only one PDF created, nothing to merge")
if err := os.Rename(fpaths[0], dest); err != nil {
return err
}
return nil
}
merger := NewMergePrint(p.logger, fpaths)
return merger.Print(ctx, dest, nil)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func unoconv(ctx context.Context, logger xlog.Logger, fpath, dest string, opts OfficePrintOptions) error {
const op string = "print.unoconv"
resolver := func() error {
args := []string{
"--format",
"pdf",
}
if opts.Landscape {
args = append(args, "--printer", "PaperOrientation=landscape")
}
args = append(args, "--output", dest, fpath)
cmd, err := xexec.CommandContext(
ctx,
logger,
"unoconv",
args...,
)
if err != nil {
return err
}
xexec.LogBeforeExecute(logger, cmd)
return cmd.Run()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Print(new(officePrint))
)

View File

@@ -0,0 +1,13 @@
package print
import (
"context"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
)
// Print is a type that can create a PDF file from a source.
// The source is defined in the underlying implementation.
type Print interface {
Print(ctx context.Context, dest string, proc process.Process) error
}

15
internal/pkg/print/url.go Normal file
View File

@@ -0,0 +1,15 @@
package print
import (
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// NewURLPrint returns a Print for
// converting a URL to PDF.
func NewURLPrint(logger xlog.Logger, url string, opts ChromePrintOptions) Print {
return chromePrint{
logger: logger,
url: url,
opts: opts,
}
}

View File

@@ -0,0 +1,106 @@
package process
import (
"context"
"fmt"
"time"
"github.com/mafredri/cdp/devtool"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
const ChromeKey Key = "chrome"
type chromeProcess struct {
id string
host string
port int
}
// NewChromeProcess returns a Google Chrome
// headless process.
func NewChromeProcess(id, host string, port int) Process {
return chromeProcess{
id: id,
host: host,
port: port,
}
}
func (p chromeProcess) ID() string {
return p.id
}
func (p chromeProcess) Host() string {
return p.host
}
func (p chromeProcess) Port() int {
return p.port
}
func (p chromeProcess) binary() string {
return "google-chrome-stable"
}
func (p chromeProcess) args() []string {
return []string{
"--no-sandbox",
"--headless",
// see https://github.com/GoogleChrome/puppeteer/issues/2410.
"--font-render-hinting=medium",
fmt.Sprintf("--remote-debugging-port=%d", p.port),
"--disable-gpu",
"--disable-translate",
"--disable-extensions",
"--disable-background-networking",
"--safebrowsing-disable-auto-update",
"--disable-sync",
"--disable-default-apps",
"--hide-scrollbars",
"--metrics-recording-only",
"--mute-audio",
"--no-first-run",
}
}
func (p chromeProcess) warmupTime() time.Duration {
return 10 * time.Second
}
func (p chromeProcess) viabilityFunc() func(logger xlog.Logger) bool {
const op string = "process.chromeProcess.viabilityFunc"
return func(logger xlog.Logger) bool {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
endpoint := fmt.Sprintf("http://%s:%d" /*p.host*/, "localhost", p.port)
logger.DebugfOp(
op,
"checking '%s' viability via endpoint '%s/json/version'",
p.ID(),
endpoint,
)
v, err := devtool.New(endpoint).Version(ctx)
if err != nil {
logger.ErrorfOp(
op,
"'%s' is not viable as endpoint returned '%v'",
p.ID(),
err,
)
return false
}
logger.DebugfOp(
op,
"'%s' is viable as endpoint returned '%v'",
p.ID(),
v,
)
return true
}
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Process(new(chromeProcess))
)

View File

@@ -0,0 +1 @@
package process

324
internal/pkg/process/pm2.go Normal file
View File

@@ -0,0 +1,324 @@
package process
import (
"encoding/json"
"fmt"
"os/exec"
"sync"
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
type jlistItem struct {
Name string `json:"name"`
PM2Env struct {
Status string `json:"status"`
RestartTime int64 `json:"restart_time"`
} `json:"pm2_env"`
Monit struct {
Memory int64 `json:"memory"`
CPU float64 `json:"cpu"`
} `json:"monit"`
}
type jlist []jlistItem
func (list jlist) toList() List {
var result List
for _, current := range list {
item := ListItem{
Name: current.Name,
Status: current.PM2Env.Status,
Restart: current.PM2Env.RestartTime,
Memory: current.Monit.Memory, // TODO humanize?
CPU: current.Monit.CPU,
}
result = append(result, item)
}
return result
}
func (list jlist) isOnline(p Process) bool {
const onlineStatus string = "online"
for _, item := range list {
if item.Name == p.ID() {
return item.PM2Env.Status == onlineStatus
}
}
return false
}
func (list jlist) memory(p Process) (int64, error) {
const op string = "process.jlist.memory"
for _, item := range list {
if item.Name == p.ID() {
return item.Monit.Memory, nil
}
}
return 0, xerror.New(
op,
fmt.Errorf("'%s' does not exist in the list of PM2 processes", p.ID()),
)
}
type command string
const (
startCommand command = "start"
restartCommand command = "restart"
stopCommand command = "stop"
logsCommand command = "logs"
jlistCommand command = "jlist"
)
const maximumRestartAttempts uint = 3
type pm2Manager struct {
logger xlog.Logger
config conf.Config
pool map[Key][]Process
list *jlist
listLock *sync.Mutex
}
// NewPM2Manager returns a PM2 manager.
func NewPM2Manager(logger xlog.Logger, config conf.Config) Manager {
const op string = "process.NewPM2Manager"
m := &pm2Manager{
logger: logger,
config: config,
pool: make(map[Key][]Process),
listLock: &sync.Mutex{},
}
if !config.DisableGoogleChrome() {
processes := make([]Process, 2)
availablePort := 9222
// TODO from config
for i := 0; i < 2; i++ {
proc := chromeProcess{
host: "127.0.0.1",
port: availablePort,
}
proc.id = fmt.Sprintf("%s-%d", proc.binary(), proc.port)
processes[i] = proc
logger.DebugfOp(op, "added new process %v", proc)
availablePort++
}
m.pool[ChromeKey] = processes
}
if !config.DisableUnoconv() {
processes := make([]Process, 2)
availablePort := 2002
// TODO from config
for i := 0; i < 2; i++ {
proc := sofficeProcess{
host: "127.0.0.1",
port: availablePort,
}
proc.id = fmt.Sprintf("%s-%d", proc.binary(), proc.port)
processes[i] = proc
logger.DebugfOp(op, "added new process %v", proc)
availablePort++
}
m.pool[SofficeKey] = processes
}
// update the manager processes list
// only if there are processes.
if !config.DisableGoogleChrome() || !config.DisableUnoconv() {
go m.jlistTimer()
}
return m
}
func (m *pm2Manager) jlistTimer() {
const op string = "process.pm2Manager.jlistTimer"
duration := xtime.Duration(10)
resolver := func() error {
m.listLock.Lock()
defer m.listLock.Unlock()
out, err := exec.
Command("pm2", string(jlistCommand)).
Output()
if err != nil {
return err
}
data := &jlist{}
if err := json.Unmarshal(out, data); err != nil {
return err
}
m.list = data
return nil
}
// update every x seconds the
// list from the manager.
for range time.Tick(duration) {
if err := resolver(); err != nil {
m.logger.ErrorOp(op, xerror.New(op, err))
}
}
}
func (m *pm2Manager) Start() error {
const op string = "process.pm2Manager.Start"
for _, processes := range m.pool {
for _, proc := range processes {
if err := m.start(proc); err != nil {
return xerror.New(op, err)
}
}
}
return nil
}
func (m *pm2Manager) start(p Process) error {
const op string = "process.pm2Manager.start"
resolver := func() error {
// first, we try to start the process.
if err := m.run(startCommand, p); err != nil {
return err
}
// we wait the process to be ready.
m.warmup(p)
// if the process failed to start correctly,
// we have to restart it.
if !m.IsViable(p) && maximumRestartAttempts > 0 {
return m.Restart(p)
}
// the process is viable, let's log its
// output.
return m.logs(p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2Manager) List() List {
m.listLock.Lock()
defer m.listLock.Unlock()
return m.list.toList()
}
func (m *pm2Manager) All() []Process {
var result []Process
for _, processes := range m.pool {
result = append(result, processes...)
}
return result
}
func (m *pm2Manager) Processes(key Key) []Process {
return m.pool[key]
}
func (m *pm2Manager) Restart(p Process) error {
const op string = "process.pm2Manager.Restart"
resolver := func() error {
var attempts uint
for attempts < maximumRestartAttempts {
// we restart the process.
if err := m.run(restartCommand, p); err != nil {
return err
}
// we wait the process to be ready.
m.warmup(p)
attempts++
// if the process is viable, we
// leave.
if m.IsViable(p) {
return m.logs(p)
}
}
return fmt.Errorf("failed to start '%s'", p.ID())
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2Manager) Stop(p Process) error {
const op string = "process.pm2Manager.Stop"
if err := m.run(stopCommand, p); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2Manager) IsViable(p Process) bool {
if !p.viabilityFunc()(m.logger) {
return false
}
m.listLock.Lock()
defer m.listLock.Unlock()
return m.list.isOnline(p)
}
func (m *pm2Manager) Memory(p Process) (int64, error) {
const op string = "process.pm2Manager.Memory"
m.listLock.Lock()
defer m.listLock.Unlock()
memory, err := m.list.memory(p)
if err != nil {
return 0, xerror.New(op, err)
}
return memory, nil
}
func (m *pm2Manager) warmup(p Process) {
const op string = "process.pm2Manager.warmup"
warmupTime := p.warmupTime()
m.logger.DebugfOp(
op,
"waiting '%v' for allowing '%s' to warmup",
warmupTime,
p.ID(),
)
time.Sleep(warmupTime)
}
func (m *pm2Manager) logs(p Process) error {
const op string = "process.pm2Manager.logs"
if m.config.LogLevel() == xlog.DebugLevel {
if err := m.run(logsCommand, p); err != nil {
return xerror.New(op, err)
}
}
return nil
}
func (m *pm2Manager) run(pm2Cmd command, p Process) error {
const op string = "process.pm2Manager.run"
resolver := func() error {
args := []string{
string(pm2Cmd),
p.binary(),
}
if pm2Cmd == startCommand {
args = append(args, fmt.Sprintf("--name=%s", p.ID()))
args = append(args, "--interpreter=none", "--")
args = append(args, p.args()...)
}
cmd, err := xexec.Command(m.logger, "pm2", args...)
if err != nil {
return err
}
xexec.LogBeforeExecute(m.logger, cmd)
return cmd.Start()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Manager(new(pm2Manager))
)

View File

@@ -0,0 +1,40 @@
package process
import (
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
type Process interface {
ID() string
Host() string
Port() int
binary() string
args() []string
warmupTime() time.Duration
viabilityFunc() func(logger xlog.Logger) bool
}
type ListItem struct {
Name string `json:"name"`
Status string `json:"status"`
Restart int64 `json:"restart"`
Memory int64 `json:"memory"`
CPU float64 `json:"cpu"`
}
type List []ListItem
type Key string
type Manager interface {
Start() error
List() List
All() []Process
Processes(key Key) []Process
Restart(proc Process) error
Stop(proc Process) error
IsViable(proc Process) bool
Memory(proc Process) (int64, error)
}

View File

@@ -0,0 +1,74 @@
package process
import (
"fmt"
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
const SofficeKey Key = "soffice"
type sofficeProcess struct {
id string
host string
port int
}
// NewSofficeProcess returns a LibreOffice
// headless process.
func NewSofficeProcess(id, host string, port int) Process {
return sofficeProcess{
id: id,
host: host,
port: port,
}
}
func (p sofficeProcess) ID() string {
return p.id
}
func (p sofficeProcess) Host() string {
return p.host
}
func (p sofficeProcess) Port() int {
return p.port
}
func (p sofficeProcess) binary() string {
return "soffice"
}
func (p sofficeProcess) args() []string {
return []string{
// see https://ask.libreoffice.org/en/question/42975/how-can-i-run-multiple-instances-of-sofficebin-at-a-time/.
fmt.Sprintf("-env:UserInstallation=file:///tmp/%d", p.port),
"--headless",
"--invisible",
"--nocrashreport",
"--nodefault",
"--nofirststartwizard",
"--nologo",
"--norestore",
fmt.Sprintf("--accept=socket,host=%s,port=%d,tcpNoDelay=1;urp;StarOffice.ComponentContext", p.host, p.port),
}
}
func (p sofficeProcess) warmupTime() time.Duration {
return 3 * time.Second
}
func (p sofficeProcess) viabilityFunc() func(logger xlog.Logger) bool {
const op string = "process.sofficeProcess.viabilityFunc"
return func(logger xlog.Logger) bool {
// TODO find a way to check.
return true
}
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Process(new(sofficeProcess))
)

View File

@@ -0,0 +1,6 @@
/*
Package xerrgroup helps running
many functions simultaneously and wait until
execution has completed or an error is encountered.
*/
package xerrgroup

View File

@@ -0,0 +1,20 @@
package xerrgroup
import (
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"golang.org/x/sync/errgroup"
)
// Run runs all functions simultaneously and wait until
// execution has completed or an error is encountered.
func Run(fn ...func() error) error {
const op string = "xerrgroup.Run"
eg := errgroup.Group{}
for _, f := range fn {
eg.Go(f)
}
if err := eg.Wait(); err != nil {
return xerror.New(op, err)
}
return nil
}