Skip to content

Commit

Permalink
start: interactive mode initial implementation
Browse files Browse the repository at this point in the history
Closes #869

@TarantoolBot document
Title: `tt start` interactive mode

This patch adds support for `tt start` interactive mode with
`-i/--interactive` flag. In this mode `tt` will wait for
child Tarantool process completions. Standard output will be
used instead of the default log files. Use Ctrl-C combination
to stop `tt` and its child Tarantool instances. No watchdog
processes created.
  • Loading branch information
psergee committed Jul 30, 2024
1 parent 46ec7fe commit 279baac
Show file tree
Hide file tree
Showing 18 changed files with 467 additions and 323 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `tt rs rebootstrap`: re-bootstraps an instance.
- `-s (--self)` flag to execute `tt` itself and don't search for other `tt`s in bin_dir
provided in config.
- `tt start` interactive mode with `-i` option.

### Fixed

Expand Down
42 changes: 41 additions & 1 deletion cli/cmd/start.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package cmd

import (
"context"
"fmt"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"

"github.com/spf13/cobra"
"github.com/tarantool/tt/cli/cmd/internal"
"github.com/tarantool/tt/cli/cmdcontext"
"github.com/tarantool/tt/cli/modules"
"github.com/tarantool/tt/cli/running"
"github.com/tarantool/tt/cli/tail"
"github.com/tarantool/tt/cli/util"
"github.com/tarantool/tt/lib/integrity"
)
Expand All @@ -23,6 +28,10 @@ var (
// integrityCheckPeriod is a flag enables periodic integrity checks.
// The default period is 1 day.
integrityCheckPeriod = 24 * 60 * 60
// startInteractive is startInteractive mode flag. If set, the main process does not exit after
// watchdog children start and waits for them to complete. Also all logging is performed
// to standard output.
startInteractive bool
)

// NewStartCmd creates start command.
Expand All @@ -49,6 +58,7 @@ func NewStartCmd() *cobra.Command {

startCmd.Flags().BoolVar(&watchdog, "watchdog", false, "")
startCmd.Flags().MarkHidden("watchdog")
startCmd.Flags().BoolVarP(&startInteractive, "interactive", "i", false, "")

integrity.RegisterIntegrityCheckPeriodFlag(startCmd.Flags(), &integrityCheckPeriod)

Expand Down Expand Up @@ -76,6 +86,36 @@ func startInstancesUnderWatchdog(cmdCtx *cmdcontext.CmdCtx, instances []running.
return nil
}

// startInstancesInteractive starts tarantool instances and waits for them to complete.
func startInstancesInteractive(cmdCtx *cmdcontext.CmdCtx, instances []running.InstanceCtx) error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

wg := sync.WaitGroup{}
pickColor := tail.DefaultColorPicker()
for _, instCtx := range instances {
clr := pickColor()
prefix := running.GetAppInstanceName(instCtx) + " "
wg.Add(1)
go func(inst running.InstanceCtx) {
running.RunInstance(ctx, cmdCtx, inst,
running.NewColorizedPrefixWriter(os.Stdout, clr, prefix),
running.NewColorizedPrefixWriter(os.Stderr, clr, prefix))
wg.Done()
}(instCtx)
}
wg.Wait()
return nil
}

// startInstances starts tarantool instances.
func startInstances(cmdCtx *cmdcontext.CmdCtx, instances []running.InstanceCtx) error {
if startInteractive {
return startInstancesInteractive(cmdCtx, instances)
}
return startInstancesUnderWatchdog(cmdCtx, instances)
}

// internalStartModule is a default start module.
func internalStartModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
if !isConfigExist(cmdCtx) {
Expand All @@ -97,7 +137,7 @@ func internalStartModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
}

if !watchdog {
if err := startInstancesUnderWatchdog(cmdCtx, runningCtx.Instances); err != nil {
if err := startInstances(cmdCtx, runningCtx.Instances); err != nil {
return err
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion cli/daemon/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (process *Process) Start() error {
return fmt.Errorf("failed to create log: %s", err)
}

if err := process_utils.CreatePIDFile(process.pidFileName); err != nil {
if err := process_utils.CreatePIDFile(process.pidFileName, os.Getpid()); err != nil {
return err
}

Expand Down
4 changes: 2 additions & 2 deletions cli/process_utils/process_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func CheckPIDFile(pidFileName string) error {

// CreatePIDFile checks that the instance PID file is absent or
// deprecated and creates a new one. Returns an error on failure.
func CreatePIDFile(pidFileName string) error {
func CreatePIDFile(pidFileName string, pid int) error {
if err := CheckPIDFile(pidFileName); err != nil {
return err
}
Expand Down Expand Up @@ -135,7 +135,7 @@ func CreatePIDFile(pidFileName string) error {
}
defer pidFile.Close()

if _, err = pidFile.WriteString(strconv.Itoa(os.Getpid())); err != nil {
if _, err = pidFile.WriteString(strconv.Itoa(pid)); err != nil {
return err
}

Expand Down
165 changes: 165 additions & 0 deletions cli/running/base_instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package running

import (
"fmt"
"io"
"os"
"strings"
"syscall"
"time"

"github.com/apex/log"
"github.com/tarantool/tt/cli/ttlog"
"github.com/tarantool/tt/lib/integrity"
)

// baseInstance represents a tarantool instance.
type baseInstance struct {
// processController is a child process controller.
*processController
// logger represents an active logging object.
logger ttlog.Logger
// tarantoolPath describes the path to the tarantool binary
// that will be used to launch the Instance.
tarantoolPath string
// appPath describes the path to the "init" file of an application.
appPath string
// appDir is an application directory.
appDir string
// appName describes the application name (the name of the directory
// where the application files are present).
appName string
// instName describes the instance name.
instName string
// walDir is a directory where write-ahead log (.xlog) files are stored.
walDir string
// memtxDir is a directory where memtx stores snapshot (.snap) files.
memtxDir string `mapstructure:"memtx_dir" yaml:"memtx_dir"`
// vinylDir is a directory where vinyl files or subdirectories will be stored.
vinylDir string `mapstructure:"vinyl_dir" yaml:"vinyl_dir"`
// consoleSocket is a Unix domain socket to be used as "admin port".
consoleSocket string
// binaryPort is a Unix socket to be used as "binary port".
binaryPort string
// logDir is log files location.
logDir string
// IntegrityCtx contains information necessary to perform integrity checks.
integrityCtx integrity.IntegrityCtx
// integrityChecks tells whether integrity checks are turned on.
integrityChecks bool
// stdOut is a standard output writer.
stdOut io.Writer
// stdErr is a standard error writer.
stdErr io.Writer
}

func newBaseInstance(tarantoolPath string, instanceCtx InstanceCtx,
opts ...InstanceOption) baseInstance {
baseInst := baseInstance{
tarantoolPath: tarantoolPath,
appPath: instanceCtx.InstanceScript,
appName: instanceCtx.AppName,
appDir: instanceCtx.AppDir,
instName: instanceCtx.InstName,
consoleSocket: instanceCtx.ConsoleSocket,
walDir: instanceCtx.WalDir,
vinylDir: instanceCtx.VinylDir,
memtxDir: instanceCtx.MemtxDir,
logDir: instanceCtx.LogDir,
binaryPort: instanceCtx.BinaryPort,
stdOut: os.Stdout,
stdErr: os.Stderr,
}
for _, opt := range opts {
opt(&baseInst)
}
return baseInst
}

// InstanceOption is a functional option to configure tarantool instance.
type InstanceOption func(inst *baseInstance) error

// IntegrityOpt sets integrity context.
func IntegrityOpt(integrityCtx integrity.IntegrityCtx) InstanceOption {
return func(inst *baseInstance) error {
inst.integrityChecks = true
inst.integrityCtx = integrityCtx
return nil
}
}

// StdOutOpt sets stdout writer for the child process.
func StdOutOpt(writer io.Writer) InstanceOption {
return func(inst *baseInstance) error {
inst.stdOut = writer
return nil
}
}

// StdErrOpt sets stderr writer for the child process.
func StdErrOpt(writer io.Writer) InstanceOption {
return func(inst *baseInstance) error {
inst.stdErr = writer
return nil
}
}

// StdLoggerOpt sets logger for the instance and standard out FDs to logger writer.
func StdLoggerOpt(logger ttlog.Logger) InstanceOption {
return func(inst *baseInstance) error {
inst.logger = logger
inst.stdOut = logger.Writer()
inst.stdErr = logger.Writer()
return nil
}
}

// Wait waits for the child process to complete.
func (inst *baseInstance) Wait() error {
if inst.processController == nil {
return fmt.Errorf("instance is not started")
}
return inst.processController.Wait()
}

// SendSignal sends a signal to tarantool instance.
func (inst *baseInstance) SendSignal(sig os.Signal) error {
if inst.processController == nil {
return fmt.Errorf("instance is not started")
}
return inst.processController.SendSignal(sig)
}

// IsAlive verifies that the instance is alive by sending a "0" signal.
func (inst *baseInstance) IsAlive() bool {
if inst.processController == nil {
return false
}
return inst.processController.IsAlive()
}

// StopWithSignal terminates the process with a specific signal.
func (inst *baseInstance) StopWithSignal(waitTimeout time.Duration, usedSignal os.Signal) error {
if inst.processController == nil {
return nil
}
return inst.processController.StopWithSignal(waitTimeout, usedSignal)
}

// Run runs tarantool instance.
func (inst *baseInstance) Run(opts RunOpts) error {
f, err := inst.integrityCtx.Repository.Read(inst.tarantoolPath)
if err != nil {
return err
}
f.Close()
newInstanceEnv := os.Environ()
args := []string{inst.tarantoolPath}
args = append(args, opts.RunArgs...)
log.Debugf("Running Tarantool with args: %s", strings.Join(args[1:], " "))
execErr := syscall.Exec(inst.tarantoolPath, args, newInstanceEnv)
if execErr != nil {
return execErr
}
return nil
}
Loading

0 comments on commit 279baac

Please sign in to comment.