Initial pgterminate code
This commit is contained in:
parent
0487d635fc
commit
565c45a8fc
15 changed files with 697 additions and 0 deletions
11
.pre-commit-config.yaml
Normal file
11
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
- repo: git://github.com/dnephin/pre-commit-golang
|
||||
sha: HEAD
|
||||
hooks:
|
||||
- id: go-fmt
|
||||
- id: go-lint
|
||||
|
||||
- repo: git://github.com/adrienverge/yamllint
|
||||
sha: HEAD
|
||||
hooks:
|
||||
- id: yamllint
|
51
README.md
Normal file
51
README.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# pgterminate
|
||||
> Terminates active and idle PostgreSQL backends
|
||||
|
||||
Did you encountered long-running queries locking down your entire company system because of a massive lock on the database? Or a scatterbrained developper connected to the production database with an open transaction leading to a production outage? Never? Really? Either you have very good policies and that's awesome, or you don't work in databases at all.
|
||||
|
||||
With `pgterminate`, you shouldn't be paged at night because some queries has locked down production for too long. It looks after "active" and "idle" connections and terminate them. As simple as that.
|
||||
|
||||
# Highlights
|
||||
* `pgterminate` name is derived from `pg_terminate_backend` function, it terminates backends.
|
||||
* backends are called sessions in `pgterminate`.
|
||||
* `active` sessions are backends in `active` state for more than `active-timeout` seconds.
|
||||
* `idle` sessions are backends in `idle`, `idle in transaction` or `idle in transaction (abort)` state for more than `idle-timeout` seconds.
|
||||
* at least one of `active-timeout` and `idle-timeout` parameter is required, both can be used.
|
||||
* `pgterminate` relies on `libpq` for PostgreSQL connection. When `-host` is ommited, connection via unix socket is used. When `-user` is ommited, the unix user is used. And so on.
|
||||
* time parameters, like `connect-timeout`, `active-timeout`, `idle-timeout` and `interval`, are represented in seconds. They accept float value except for `connect-timeout` which is an integer.
|
||||
* if you want `pgterminate` to terminate any session, ensure it has SUPERUSER privileges.
|
||||
|
||||
# Internals
|
||||
|
||||
## Signals
|
||||
`pgterminate` handles the following OS signals:
|
||||
* `SIGINT`, `SIGTERM` to gracefully terminates the infinite loop
|
||||
* `SIGHUP` to reload configuration file and re-open log file if used (handy for logrotate)
|
||||
|
||||
## Configuration
|
||||
There's two ways to configure `pgterminate`:
|
||||
* command-line arguments
|
||||
* configuration file with `-config` command-line argument
|
||||
|
||||
Configuration file options **override** command-line arguments
|
||||
|
||||
# Usage
|
||||
Connect to a remote instance and prompt for password:
|
||||
```
|
||||
pgterminate -host 10.0.0.1 -port 5432 -user test -prompt-password -database test
|
||||
```
|
||||
Use a configuration file:
|
||||
```
|
||||
pgterminate -config config.yaml
|
||||
```
|
||||
Use both configuration file and command-line arguments:
|
||||
```
|
||||
pgterminate -config config.yaml -interval 0.25 -active-timeout 10 -idle-timeout 300
|
||||
```
|
||||
Print usage:
|
||||
```
|
||||
pgterminate -help
|
||||
```
|
||||
|
||||
# License
|
||||
`pgterminate` is released under [The Unlicense](https://github.com/jouir/pgterminate/blob/master/LICENSE) license. Code is under public domain.
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
0.1.0
|
97
base/config.go
Normal file
97
base/config.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// AppName exposes application name to config module
|
||||
var AppName string
|
||||
|
||||
// Config receives configuration options
|
||||
type Config struct {
|
||||
mutex sync.Mutex
|
||||
File string
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
Database string `yaml:"database"`
|
||||
Interval float64 `yaml:"interval"`
|
||||
ConnectTimeout int `yaml:"connect-timeout"`
|
||||
IdleTimeout float64 `yaml:"idle-timeout"`
|
||||
ActiveTimeout float64 `yaml:"active-timeout"`
|
||||
LogFile string `yaml:"log-file"`
|
||||
PidFile string `yaml:"pid-file"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
AppName = "pgterminate"
|
||||
}
|
||||
|
||||
// NewConfig creates a Config object
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
// Read loads options from a configuration file to Config
|
||||
func (c *Config) Read(file string) error {
|
||||
file, err := filepath.Abs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yamlFile, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(yamlFile, &c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload reads from file and update configuration
|
||||
func (c *Config) Reload() {
|
||||
log.Println("Reloading configuration")
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if c.File != "" {
|
||||
c.Read(c.File)
|
||||
}
|
||||
}
|
||||
|
||||
// Dsn formats a connection string based on Config
|
||||
func (c *Config) Dsn() string {
|
||||
var parameters []string
|
||||
if c.Host != "" {
|
||||
parameters = append(parameters, fmt.Sprintf("host=%s", c.Host))
|
||||
}
|
||||
if c.Port != 0 {
|
||||
parameters = append(parameters, fmt.Sprintf("port=%d", c.Port))
|
||||
}
|
||||
if c.User != "" {
|
||||
parameters = append(parameters, fmt.Sprintf("user=%s", c.User))
|
||||
}
|
||||
if c.Password != "" {
|
||||
parameters = append(parameters, fmt.Sprintf("password=%s", c.Password))
|
||||
}
|
||||
if c.Database != "" {
|
||||
parameters = append(parameters, fmt.Sprintf("database=%s", c.Database))
|
||||
}
|
||||
if c.ConnectTimeout != 0 {
|
||||
parameters = append(parameters, fmt.Sprintf("connect_timeout=%d", c.ConnectTimeout))
|
||||
}
|
||||
if AppName != "" {
|
||||
parameters = append(parameters, fmt.Sprintf("application_name=%s", AppName))
|
||||
}
|
||||
return strings.Join(parameters, " ")
|
||||
}
|
17
base/context.go
Normal file
17
base/context.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package base
|
||||
|
||||
// Context stores dynamic values like channels and exposes configuration
|
||||
type Context struct {
|
||||
Sessions chan Session
|
||||
Done chan bool
|
||||
Config *Config
|
||||
}
|
||||
|
||||
// NewContext instanciates a Context
|
||||
func NewContext(config *Config, sessions chan Session, done chan bool) *Context {
|
||||
return &Context{
|
||||
Config: config,
|
||||
Sessions: sessions,
|
||||
Done: done,
|
||||
}
|
||||
}
|
76
base/db.go
Normal file
76
base/db.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/lib/pq"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
maxQueryLength = 1000
|
||||
)
|
||||
|
||||
// Db centralizes connection to the database
|
||||
type Db struct {
|
||||
dsn string
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// NewDb creates a Db object
|
||||
func NewDb(dsn string) *Db {
|
||||
return &Db{
|
||||
dsn: dsn,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects to the instance and ping it to ensure connection is working
|
||||
func (db *Db) Connect() {
|
||||
conn, err := sql.Open("postgres", db.dsn)
|
||||
Panic(err)
|
||||
|
||||
err = conn.Ping()
|
||||
Panic(err)
|
||||
|
||||
db.conn = conn
|
||||
}
|
||||
|
||||
// Disconnect ends connection cleanly
|
||||
func (db *Db) Disconnect() {
|
||||
err := db.conn.Close()
|
||||
Panic(err)
|
||||
}
|
||||
|
||||
// Sessions connects to the database and returns current sessions
|
||||
func (db *Db) Sessions() (sessions []Session) {
|
||||
query := `select pid as pid, usename as user, datname as db, host(client_addr)::text || ':' || client_port::text as client, state as state, substring(query from 1 for ` + strconv.Itoa(maxQueryLength) + `) as query, coalesce(extract(epoch from now() - backend_start), 0) as "backendDuration", coalesce(extract(epoch from now() - xact_start), 0) as "xactDuration", coalesce(extract(epoch from now() - query_start), 0) as "queryDuration" from pg_catalog.pg_stat_activity where pid <> pg_backend_pid();`
|
||||
rows, err := db.conn.Query(query)
|
||||
Panic(err)
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var pid sql.NullInt64
|
||||
var user, db, client, state, query sql.NullString
|
||||
var backendDuration, xactDuration, queryDuration float64
|
||||
err := rows.Scan(&pid, &user, &db, &client, &state, &query, &backendDuration, &xactDuration, &queryDuration)
|
||||
Panic(err)
|
||||
|
||||
if pid.Valid && user.Valid && db.Valid && client.Valid && state.Valid && query.Valid {
|
||||
sessions = append(sessions, NewSession(pid.Int64, user.String, db.String, client.String, state.String, query.String, backendDuration, xactDuration, queryDuration))
|
||||
}
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
// TerminateSessions terminates a list of sessions
|
||||
func (db *Db) TerminateSessions(sessions []Session) {
|
||||
var pids []int64
|
||||
for _, session := range sessions {
|
||||
pids = append(pids, session.Pid)
|
||||
}
|
||||
if len(pids) > 0 {
|
||||
query := `select pg_terminate_backend(pid) from pg_stat_activity where pid = any($1);`
|
||||
_, err := db.conn.Exec(query, pq.Array(pids))
|
||||
Panic(err)
|
||||
}
|
||||
}
|
67
base/session.go
Normal file
67
base/session.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Session represents a PostgreSQL backend
|
||||
type Session struct {
|
||||
Pid int64
|
||||
User string
|
||||
Db string
|
||||
Client string
|
||||
State string
|
||||
Query string
|
||||
BackendDuration float64
|
||||
XactDuration float64
|
||||
QueryDuration float64
|
||||
}
|
||||
|
||||
// NewSession instanciates a Session
|
||||
func NewSession(pid int64, user string, db string, client string, state string, query string, backendDuration float64, xactDuration float64, queryDuration float64) Session {
|
||||
return Session{
|
||||
Pid: pid,
|
||||
User: user,
|
||||
Db: db,
|
||||
Client: client,
|
||||
State: state,
|
||||
Query: query,
|
||||
BackendDuration: backendDuration,
|
||||
XactDuration: xactDuration,
|
||||
QueryDuration: queryDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// String represents a Session as a string
|
||||
func (s Session) String() string {
|
||||
var output []string
|
||||
if s.Pid != 0 {
|
||||
output = append(output, fmt.Sprintf("pid=%d", s.Pid))
|
||||
}
|
||||
if s.User != "" {
|
||||
output = append(output, fmt.Sprintf("user=%s", s.User))
|
||||
}
|
||||
if s.Db != "" {
|
||||
output = append(output, fmt.Sprintf("db=%s", s.Db))
|
||||
}
|
||||
if s.Client != "" {
|
||||
output = append(output, fmt.Sprintf("client=%s", s.Client))
|
||||
}
|
||||
if s.State != "" {
|
||||
output = append(output, fmt.Sprintf("state=%s", s.State))
|
||||
}
|
||||
if s.BackendDuration != 0 {
|
||||
output = append(output, fmt.Sprintf("backend_duration=%f", s.BackendDuration))
|
||||
}
|
||||
if s.XactDuration != 0 {
|
||||
output = append(output, fmt.Sprintf("xact_duration=%f", s.XactDuration))
|
||||
}
|
||||
if s.QueryDuration != 0 {
|
||||
output = append(output, fmt.Sprintf("query_duration=%f", s.QueryDuration))
|
||||
}
|
||||
if s.Query != "" {
|
||||
output = append(output, fmt.Sprintf("query=%s", s.Query))
|
||||
}
|
||||
return strings.Join(output, " ")
|
||||
}
|
12
base/utils.go
Normal file
12
base/utils.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Panic prints a non-nil error and terminates the program
|
||||
func Panic(err error) {
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
16
build.sh
Executable file
16
build.sh
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
BINARY=pgterminate
|
||||
VERSION=$(cat VERSION)
|
||||
BUILD_PATH=/tmp/${BINARY}-${VERSION}
|
||||
ldflags="-X main.AppVersion=${VERSION}"
|
||||
GOOS=linux
|
||||
GOARCH=amd64
|
||||
|
||||
export GOOS
|
||||
export GOARCH
|
||||
|
||||
go build -ldflags "$ldflags" -o ${BUILD_PATH}/${BINARY} cmd/${BINARY}/main.go
|
||||
(cd ${BUILD_PATH} && tar czf ${BINARY}-${VERSION}-${GOOS}-${GOARCH}.tar.gz ${BINARY})
|
||||
|
||||
echo "Archive created:"
|
||||
ls -l ${BUILD_PATH}/${BINARY}-${VERSION}-${GOOS}-${GOARCH}.tar.gz
|
140
cmd/pgterminate/main.go
Normal file
140
cmd/pgterminate/main.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/jouir/pgterminate/base"
|
||||
"github.com/jouir/pgterminate/notifier"
|
||||
"github.com/jouir/pgterminate/terminator"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// AppVersion stores application version at compilation time
|
||||
var AppVersion string
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
config := base.NewConfig()
|
||||
|
||||
version := flag.Bool("version", false, "Print version")
|
||||
flag.StringVar(&config.File, "config", "", "Configuration file")
|
||||
flag.StringVar(&config.Host, "host", "", "Instance host address")
|
||||
flag.IntVar(&config.Port, "port", 0, "Instance port")
|
||||
flag.StringVar(&config.User, "user", "", "Instance username")
|
||||
flag.StringVar(&config.Password, "password", "", "Instance password")
|
||||
flag.StringVar(&config.Database, "database", "", "Instance database")
|
||||
prompt := flag.Bool("prompt-password", false, "Prompt for password")
|
||||
flag.Float64Var(&config.Interval, "interval", 1, "Time to sleep between iterations in seconds")
|
||||
flag.IntVar(&config.ConnectTimeout, "connect-timeout", 3, "Connection timeout in seconds")
|
||||
flag.Float64Var(&config.IdleTimeout, "idle-timeout", 0, "Time for idle connections to be terminated in seconds")
|
||||
flag.Float64Var(&config.ActiveTimeout, "active-timeout", 0, "Time for active connections to be terminated in seconds")
|
||||
flag.StringVar(&config.LogFile, "log-file", "", "Write logs to a file")
|
||||
flag.StringVar(&config.PidFile, "pid-file", "", "Write process id into a file")
|
||||
flag.Parse()
|
||||
|
||||
if *version {
|
||||
if AppVersion == "" {
|
||||
AppVersion = "unknown"
|
||||
}
|
||||
fmt.Println(AppVersion)
|
||||
return
|
||||
}
|
||||
|
||||
if *prompt {
|
||||
fmt.Print("Password:")
|
||||
bytes, err := terminal.ReadPassword(syscall.Stdin)
|
||||
base.Panic(err)
|
||||
config.Password = string(bytes)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
if config.File != "" {
|
||||
err = config.Read(config.File)
|
||||
base.Panic(err)
|
||||
}
|
||||
|
||||
if config.ActiveTimeout == 0 && config.IdleTimeout == 0 {
|
||||
log.Fatalln("Parameter -active-timeout or -idle-timeout required")
|
||||
}
|
||||
|
||||
if config.PidFile != "" {
|
||||
writePid(config.PidFile)
|
||||
defer removePid(config.PidFile)
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
sessions := make(chan base.Session)
|
||||
|
||||
ctx := base.NewContext(config, sessions, done)
|
||||
terminator := terminator.NewTerminator(ctx)
|
||||
notifier := notifier.NewNotifier(ctx)
|
||||
|
||||
handleSignals(ctx, notifier)
|
||||
|
||||
// Run managers asynchronously and wait for all of them to end
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
terminator.Run()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
notifier.Run()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// handleSignals handles operating system signals
|
||||
func handleSignals(ctx *base.Context, n notifier.Notifier) {
|
||||
// When interrupt or terminated, terminate managers, close channel and terminate program
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
signal.Notify(c, syscall.SIGTERM)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
log.Printf("Received %v signal\n", sig)
|
||||
close(ctx.Sessions)
|
||||
ctx.Done <- true
|
||||
}
|
||||
}()
|
||||
|
||||
// When hangup, reload notifier
|
||||
h := make(chan os.Signal, 1)
|
||||
signal.Notify(h, syscall.SIGHUP)
|
||||
go func() {
|
||||
for sig := range h {
|
||||
log.Printf("Received %v signal\n", sig)
|
||||
ctx.Config.Reload()
|
||||
n.Reload()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// writePid writes current pid into a pid file
|
||||
func writePid(file string) {
|
||||
log.Println("Creating pid file", file)
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
|
||||
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
base.Panic(err)
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(pid)
|
||||
base.Panic(err)
|
||||
}
|
||||
|
||||
// removePid removes pid file
|
||||
func removePid(file string) {
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
log.Println("Removing pid file", file)
|
||||
err := os.Remove(file)
|
||||
base.Panic(err)
|
||||
}
|
||||
}
|
11
config.yaml.example
Normal file
11
config.yaml.example
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
host: 127.0.0.1
|
||||
port: 5432
|
||||
user: test
|
||||
password: ****
|
||||
interval: 1000
|
||||
connect-timeout: 3
|
||||
idle-timeout: 300
|
||||
active-timeout: 10
|
||||
log-file: /var/log/pgterminate/pgterminate.log
|
||||
pid-file: /var/run/pgterminate/pgterminate.pid
|
29
notifier/console.go
Normal file
29
notifier/console.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"github.com/jouir/pgterminate/base"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Console notifier structure
|
||||
type Console struct {
|
||||
sessions chan base.Session
|
||||
}
|
||||
|
||||
// NewConsole creates a console notifier
|
||||
func NewConsole(sessions chan base.Session) Notifier {
|
||||
return &Console{
|
||||
sessions: sessions,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts console notifier
|
||||
func (c *Console) Run() {
|
||||
for session := range c.sessions {
|
||||
log.Printf("%s", session)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload for handling SIGHUP signals
|
||||
func (c *Console) Reload() {
|
||||
}
|
55
notifier/file.go
Normal file
55
notifier/file.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"github.com/jouir/pgterminate/base"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// File structure for file notifier
|
||||
type File struct {
|
||||
handle *os.File
|
||||
name string
|
||||
sessions chan base.Session
|
||||
}
|
||||
|
||||
// NewFile creates a file notifier
|
||||
func NewFile(name string, sessions chan base.Session) Notifier {
|
||||
return &File{
|
||||
name: name,
|
||||
sessions: sessions,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the file notifier
|
||||
func (f *File) Run() {
|
||||
f.open()
|
||||
defer f.terminate()
|
||||
|
||||
for session := range f.sessions {
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
_, err := f.handle.WriteString(timestamp + " " + session.String() + "\n")
|
||||
base.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// open opens a log file
|
||||
func (f *File) open() {
|
||||
var err error
|
||||
f.handle, err = os.OpenFile(f.name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
base.Panic(err)
|
||||
}
|
||||
|
||||
// Reload closes and re-open the file to be compatible with logrotate
|
||||
func (f *File) Reload() {
|
||||
log.Println("Re-opening log file", f.name)
|
||||
f.handle.Close()
|
||||
f.open()
|
||||
}
|
||||
|
||||
// terminate closes the file
|
||||
func (f *File) terminate() {
|
||||
log.Println("Closing log file", f.name)
|
||||
f.handle.Close()
|
||||
}
|
20
notifier/notifier.go
Normal file
20
notifier/notifier.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"github.com/jouir/pgterminate/base"
|
||||
)
|
||||
|
||||
// Notifier generic interface for implementing a notifier
|
||||
type Notifier interface {
|
||||
Run()
|
||||
Reload()
|
||||
}
|
||||
|
||||
// NewNotifier looks into Config to create a File or Console notifier and pass it
|
||||
// the session channel for consuming sessions structs sent by terminator
|
||||
func NewNotifier(ctx *base.Context) Notifier {
|
||||
if ctx.Config.LogFile != "" {
|
||||
return NewFile(ctx.Config.LogFile, ctx.Sessions)
|
||||
}
|
||||
return NewConsole(ctx.Sessions)
|
||||
}
|
94
terminator/terminator.go
Normal file
94
terminator/terminator.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package terminator
|
||||
|
||||
import (
|
||||
"github.com/jouir/pgterminate/base"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Terminator looks for sessions, filters actives and idles, terminate them and notify sessions channel
|
||||
// It ends itself gracefully when done channel is triggered
|
||||
type Terminator struct {
|
||||
config *base.Config
|
||||
db *base.Db
|
||||
sessions chan base.Session
|
||||
done chan bool
|
||||
}
|
||||
|
||||
// NewTerminator instanciates a Terminator
|
||||
func NewTerminator(ctx *base.Context) *Terminator {
|
||||
return &Terminator{
|
||||
config: ctx.Config,
|
||||
sessions: ctx.Sessions,
|
||||
done: ctx.Done,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the Terminator
|
||||
func (t *Terminator) Run() {
|
||||
t.db = base.NewDb(t.config.Dsn())
|
||||
log.Println("Connecting to instance")
|
||||
t.db.Connect()
|
||||
defer t.terminate()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.done:
|
||||
return
|
||||
default:
|
||||
sessions := t.db.Sessions()
|
||||
if t.config.ActiveTimeout != 0 {
|
||||
actives := activeSessions(sessions, t.config.ActiveTimeout)
|
||||
go t.terminateAndNotify(actives)
|
||||
}
|
||||
|
||||
if t.config.IdleTimeout != 0 {
|
||||
idles := idleSessions(sessions, t.config.IdleTimeout)
|
||||
go t.terminateAndNotify(idles)
|
||||
}
|
||||
time.Sleep(time.Duration(t.config.Interval*1000) * time.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// terminateAndNotify terminates a list of sessions and notifies channel
|
||||
func (t *Terminator) terminateAndNotify(sessions []base.Session) {
|
||||
t.db.TerminateSessions(sessions)
|
||||
for _, session := range sessions {
|
||||
t.sessions <- session
|
||||
}
|
||||
}
|
||||
|
||||
// terminate terminates gracefully
|
||||
func (t *Terminator) terminate() {
|
||||
log.Println("Disconnecting from instance")
|
||||
t.db.Disconnect()
|
||||
}
|
||||
|
||||
// activeSessions returns a list of active sessions
|
||||
// A session is active when state is "active" and backend has started before elapsed
|
||||
// seconds
|
||||
func activeSessions(sessions []base.Session, elapsed float64) (result []base.Session) {
|
||||
for _, session := range sessions {
|
||||
if session.State == "active" && session.QueryDuration > elapsed {
|
||||
result = append(result, session)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// idleSessions returns a list of idle sessions
|
||||
// A sessions is idle when state is "idle" and backend has started before elapsed seconds
|
||||
// and when state is "idle in transaction" or "idle in transaction (aborted)" and
|
||||
// transaction has started before elapsed seconds
|
||||
func idleSessions(sessions []base.Session, elapsed float64) (result []base.Session) {
|
||||
for _, session := range sessions {
|
||||
if session.State == "idle" && session.BackendDuration > elapsed {
|
||||
result = append(result, session)
|
||||
} else if (session.State == "idle in transaction" || session.State == "idle in transaction (aborted)") && session.XactDuration > elapsed {
|
||||
result = append(result, session)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
Loading…
Reference in a new issue