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 }