Files
gotenberg/internal/pkg/process/pm2.go
2019-09-19 17:28:29 +02:00

325 lines
7.2 KiB
Go

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))
)