github.com/govau/cf-common@v0.0.7/jobs/database.go (about)

     1  package jobs
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"os/signal"
     9  	"sync"
    10  	"syscall"
    11  
    12  	cfenv "github.com/cloudfoundry-community/go-cfenv"
    13  
    14  	"github.com/bgentry/que-go"
    15  	"github.com/jackc/pgx"
    16  )
    17  
    18  // Return a database object, using the CloudFoundry environment data
    19  func MustPGXConfigFromCloudFoundry() *pgx.ConnConfig {
    20  	rv, err := PGXConfigFromCloudFoundry()
    21  	if err != nil {
    22  		log.Fatal(err)
    23  	}
    24  	return rv
    25  }
    26  
    27  // Return a database object, using the CloudFoundry environment data
    28  func PGXConfigFromCloudFoundry() (*pgx.ConnConfig, error) {
    29  	appEnv, err := cfenv.Current()
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	dbEnv, err := appEnv.Services.WithTag("postgres")
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	if len(dbEnv) != 1 {
    40  		return nil, errors.New("expecting 1 database")
    41  	}
    42  
    43  	creds := dbEnv[0].Credentials
    44  	return &pgx.ConnConfig{
    45  		Database: creds["name"].(string),
    46  		User:     creds["username"].(string),
    47  		Password: creds["password"].(string),
    48  		Host:     creds["host"].(string),
    49  		Port:     uint16(creds["port"].(float64)),
    50  	}, nil
    51  }
    52  
    53  type dbInitter struct {
    54  	InitSQL            string
    55  	PreparedStatements map[string]string
    56  	OtherStatements    func(*pgx.Conn) error
    57  
    58  	// Clearly this won't stop other instances in a race condition, but should at least stop ourselves from hammering ourselves unnecessarily
    59  	runMutex   sync.Mutex
    60  	runAlready bool
    61  }
    62  
    63  func (dbi *dbInitter) ensureInitDone(c *pgx.Conn) error {
    64  	dbi.runMutex.Lock()
    65  	defer dbi.runMutex.Unlock()
    66  
    67  	if dbi.runAlready {
    68  		return nil
    69  	}
    70  
    71  	_, err := c.Exec(dbi.InitSQL)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	dbi.runAlready = true
    77  	return nil
    78  }
    79  
    80  func (dbi *dbInitter) AfterConnect(c *pgx.Conn) error {
    81  	if dbi.InitSQL != "" {
    82  		err := dbi.ensureInitDone(c)
    83  		if err != nil {
    84  			return err
    85  		}
    86  	}
    87  
    88  	if dbi.OtherStatements != nil {
    89  		err := dbi.OtherStatements(c)
    90  		if err != nil {
    91  			return err
    92  		}
    93  	}
    94  
    95  	if dbi.PreparedStatements != nil {
    96  		for n, sql := range dbi.PreparedStatements {
    97  			_, err := c.Prepare(n, sql)
    98  			if err != nil {
    99  				return err
   100  			}
   101  		}
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  type Handler struct {
   108  	PGXConnConfig *pgx.ConnConfig
   109  	InitSQL       string
   110  	WorkerCount   int
   111  	WorkerMap     map[string]*JobConfig
   112  	OnStart       func(qc *que.Client, pgxPool *pgx.ConnPool, logger *log.Logger) error
   113  	Logger        *log.Logger
   114  	QueueName     string
   115  }
   116  
   117  func (h *Handler) WorkForever() error {
   118  	if h.Logger == nil {
   119  		h.Logger = log.New(os.Stderr, "", log.LstdFlags)
   120  	}
   121  
   122  	pgxPool, err := pgx.NewConnPool(pgx.ConnPoolConfig{
   123  		MaxConnections: h.WorkerCount * 2,
   124  		ConnConfig:     *h.PGXConnConfig,
   125  		AfterConnect: (&dbInitter{
   126  			InitSQL: fmt.Sprintf(`
   127  				CREATE TABLE IF NOT EXISTS que_jobs (
   128  					priority    smallint    NOT NULL DEFAULT 100,
   129  					run_at      timestamptz NOT NULL DEFAULT now(),
   130  					job_id      bigserial   NOT NULL,
   131  					job_class   text        NOT NULL,
   132  					args        json        NOT NULL DEFAULT '[]'::json,
   133  					error_count integer     NOT NULL DEFAULT 0,
   134  					last_error  text,
   135  					queue       text        NOT NULL DEFAULT '',
   136  
   137  					CONSTRAINT que_jobs_pkey PRIMARY KEY (queue, priority, run_at, job_id)
   138  				);
   139  
   140  				CREATE TABLE IF NOT EXISTS cron_metadata (
   141  					id             text                     PRIMARY KEY,
   142  					last_completed timestamp with time zone NOT NULL DEFAULT TIMESTAMP 'EPOCH',
   143  					next_scheduled timestamp with time zone NOT NULL DEFAULT TIMESTAMP 'EPOCH'
   144  				);
   145  
   146  				%s
   147  				`, h.InitSQL),
   148  			OtherStatements:    que.PrepareStatements,
   149  			PreparedStatements: map[string]string{},
   150  		}).AfterConnect,
   151  	})
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	qc := que.NewClient(pgxPool)
   157  
   158  	workerMap := make(que.WorkMap)
   159  	for k, v := range h.WorkerMap {
   160  		workerMap[k] = v.CloneWith(qc, h.Logger).Run
   161  	}
   162  
   163  	workers := que.NewWorkerPool(qc, workerMap, h.WorkerCount)
   164  	workers.Queue = h.QueueName
   165  
   166  	// Prepare a shutdown function
   167  	shutdown := func() {
   168  		workers.Shutdown()
   169  		pgxPool.Close()
   170  	}
   171  
   172  	// Normal exit
   173  	defer shutdown()
   174  
   175  	// Or via signals
   176  	sigCh := make(chan os.Signal, 1)
   177  	signal.Notify(sigCh, os.Interrupt)
   178  	signal.Notify(sigCh, syscall.SIGTERM)
   179  	go func() {
   180  		sig := <-sigCh
   181  		log.Printf("Received %v, starting shutdown...", sig)
   182  		shutdown()
   183  		log.Println("Shutdown complete")
   184  		os.Exit(0)
   185  	}()
   186  
   187  	if h.OnStart != nil {
   188  		err = h.OnStart(qc, pgxPool, h.Logger)
   189  		if err != nil {
   190  			return err
   191  		}
   192  	}
   193  
   194  	workers.Start()
   195  
   196  	// Wait forever
   197  	select {}
   198  }