Skip to content

Commit 3726184

Browse files
jeanbzajba
authored andcommitted
internal/queue: add postgres queue implementation, and use it in worker|frontend
Fixes golang/go#74027. Change-Id: I916ac81093e782d4eda21fe11ef47eeff4f5f0b1 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/751480 Reviewed-by: Jonathan Amsterdam <jba@google.com> kokoro-CI: kokoro <noreply+kokoro@google.com> Reviewed-by: Ethan Lee <ethanalee@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent e09b390 commit 3726184

6 files changed

Lines changed: 493 additions & 22 deletions

File tree

cmd/frontend/main.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"golang.org/x/pkgsite/internal/queue"
3535
"golang.org/x/pkgsite/internal/queue/gcpqueue"
3636
"golang.org/x/pkgsite/internal/queue/inmemqueue"
37+
"golang.org/x/pkgsite/internal/queue/pgqueue"
3738
"golang.org/x/pkgsite/internal/source"
3839
"golang.org/x/pkgsite/internal/static"
3940
"golang.org/x/pkgsite/internal/trace"
@@ -54,6 +55,7 @@ var (
5455
"as a direct backend, bypassing the database")
5556
bypassLicenseCheck = flag.Bool("bypass_license_check", false, "display all information, even for non-redistributable paths")
5657
hostAddr = flag.String("host", "localhost:8080", "Host address for the server")
58+
queueType = flag.String("queue", "inmemory", `queue implementation when not on GCP: "inmemory" or "postgres"`)
5759
)
5860

5961
func main() {
@@ -127,19 +129,30 @@ func main() {
127129
}
128130
fetchQueue = q
129131
} else {
130-
experiments, err := expg(ctx)
131-
if err != nil {
132-
log.Fatalf(ctx, "error getting experiment: %v", err)
132+
processFunc := func(ctx context.Context, modulePath, version string) (int, error) {
133+
return fetchserver.FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, db)
133134
}
134-
var names []string
135-
for _, e := range experiments {
136-
if e.Rollout > 0 {
137-
names = append(names, e.Name)
135+
switch *queueType {
136+
case "postgres":
137+
q, err := pgqueue.New(ctx, db.Underlying())
138+
if err != nil {
139+
log.Fatalf(ctx, "error creating postgres queue: %v", err)
140+
}
141+
go q.Poll(ctx, *workers, processFunc)
142+
fetchQueue = q
143+
default:
144+
experiments, err := expg(ctx)
145+
if err != nil {
146+
log.Fatalf(ctx, "error getting experiment: %v", err)
138147
}
148+
var names []string
149+
for _, e := range experiments {
150+
if e.Rollout > 0 {
151+
names = append(names, e.Name)
152+
}
153+
}
154+
fetchQueue = inmemqueue.New(ctx, *workers, names, processFunc)
139155
}
140-
fetchQueue = inmemqueue.New(ctx, *workers, names, func(ctx context.Context, modulePath, version string) (int, error) {
141-
return fetchserver.FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, db)
142-
})
143156
}
144157
}
145158

cmd/worker/main.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"golang.org/x/pkgsite/internal/queue"
3232
"golang.org/x/pkgsite/internal/queue/gcpqueue"
3333
"golang.org/x/pkgsite/internal/queue/inmemqueue"
34+
"golang.org/x/pkgsite/internal/queue/pgqueue"
3435
"golang.org/x/pkgsite/internal/source"
3536
"golang.org/x/pkgsite/internal/trace"
3637
"golang.org/x/pkgsite/internal/worker"
@@ -43,6 +44,7 @@ var (
4344
// flag used in call to safehtml/template.TrustedSourceFromFlag
4445
_ = flag.String("static", "static", "path to folder containing static files served")
4546
bypassLicenseCheck = flag.Bool("bypass_license_check", false, "insert all data into the DB, even for non-redistributable paths")
47+
queueType = flag.String("queue", "inmemory", `queue implementation when not on GCP: "inmemory" or "postgres"`)
4648

4749
// Ordinarily, index polling is initiated by a separate scheduler that calls
4850
// /poll. But for convenience, you can instead have the worker periodically
@@ -107,25 +109,36 @@ func main() {
107109
}
108110
fetchQueue = q
109111
} else {
110-
experiments, err := expg(ctx)
111-
if err != nil {
112-
log.Fatalf(ctx, "error getting experiment: %v", err)
113-
}
114-
var names []string
115-
for _, e := range experiments {
116-
if e.Rollout > 0 {
117-
names = append(names, e.Name)
118-
}
119-
}
120112
f := &worker.Fetcher{
121113
ProxyClient: proxyClient,
122114
SourceClient: sourceClient,
123115
DB: db,
124116
}
125-
fetchQueue = inmemqueue.New(ctx, *workers, names, func(ctx context.Context, modulePath, version string) (int, error) {
117+
processFunc := func(ctx context.Context, modulePath, version string) (int, error) {
126118
code, _, err := f.FetchAndUpdateState(ctx, modulePath, version, cfg.AppVersionLabel())
127119
return code, err
128-
})
120+
}
121+
switch *queueType {
122+
case "postgres":
123+
q, err := pgqueue.New(ctx, db.Underlying())
124+
if err != nil {
125+
log.Fatalf(ctx, "error creating postgres queue: %v", err)
126+
}
127+
go q.Poll(ctx, *workers, processFunc)
128+
fetchQueue = q
129+
default:
130+
experiments, err := expg(ctx)
131+
if err != nil {
132+
log.Fatalf(ctx, "error getting experiment: %v", err)
133+
}
134+
var names []string
135+
for _, e := range experiments {
136+
if e.Rollout > 0 {
137+
names = append(names, e.Name)
138+
}
139+
}
140+
fetchQueue = inmemqueue.New(ctx, *workers, names, processFunc)
141+
}
129142
}
130143

131144
reporter := cmdconfig.Reporter(ctx, cfg)

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/google/go-replayers/httpreplay v1.0.0
2323
github.com/google/licensecheck v0.3.1
2424
github.com/google/safehtml v0.0.3-0.20211026203422-d6f0e11a5516
25+
github.com/jackc/pgx/v4 v4.10.1
2526
github.com/jackc/pgx/v5 v5.9.1
2627
github.com/jba/templatecheck v0.6.0
2728
github.com/lib/pq v1.12.0
@@ -64,8 +65,13 @@ require (
6465
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
6566
github.com/hashicorp/errwrap v1.0.0 // indirect
6667
github.com/hashicorp/go-multierror v1.1.0 // indirect
68+
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
69+
github.com/jackc/pgconn v1.8.0 // indirect
70+
github.com/jackc/pgio v1.0.0 // indirect
6771
github.com/jackc/pgpassfile v1.0.0 // indirect
72+
github.com/jackc/pgproto3/v2 v2.0.7 // indirect
6873
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
74+
github.com/jackc/pgtype v1.6.2 // indirect
6975
github.com/jackc/puddle/v2 v2.2.2 // indirect
7076
github.com/jmespath/go-jmespath v0.4.0 // indirect
7177
github.com/kr/text v0.2.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH
224224
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
225225
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
226226
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
227+
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
227228
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
228229
github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM=
229230
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
@@ -465,6 +466,7 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6
465466
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
466467
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
467468
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
469+
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
468470
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
469471
github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
470472
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
@@ -632,16 +634,20 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
632634
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
633635
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
634636
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
637+
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
635638
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
636639
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
637640
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
638641
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
639642
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
640643
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
641644
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
645+
github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw=
642646
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
643647
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
648+
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
644649
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
650+
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
645651
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
646652
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
647653
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@@ -652,6 +658,7 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
652658
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
653659
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
654660
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
661+
github.com/jackc/pgproto3/v2 v2.0.7 h1:6Pwi1b3QdY65cuv6SyVO0FgPd5J3Bl7wf/nQQjinHMA=
655662
github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
656663
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
657664
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
@@ -663,13 +670,15 @@ github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrU
663670
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
664671
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
665672
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
673+
github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8=
666674
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
667675
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
668676
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
669677
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
670678
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
671679
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
672680
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
681+
github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY=
673682
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
674683
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
675684
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
@@ -935,6 +944,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
935944
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
936945
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
937946
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
947+
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
938948
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
939949
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
940950
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=

internal/queue/pgqueue/queue.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2026 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package pgqueue provides a Postgres-backed queue implementation for
6+
// scheduling and processing fetch actions. It supports multiple concurrent
7+
// workers (processes or goroutines)
8+
package pgqueue
9+
10+
import (
11+
"context"
12+
"database/sql"
13+
"errors"
14+
"fmt"
15+
"sync"
16+
"time"
17+
18+
"golang.org/x/pkgsite/internal/database"
19+
"golang.org/x/pkgsite/internal/log"
20+
"golang.org/x/pkgsite/internal/queue"
21+
)
22+
23+
// The frequency at which we poll for work.
24+
const pollInterval = 5 * time.Second
25+
26+
// ProcessFunc is the function signature for processing dequeued work.
27+
type ProcessFunc func(ctx context.Context, modulePath, version string) (int, error)
28+
29+
// Queue implements the Queue interface backed by a Postgres table. It is safe
30+
// for concurrent use by multiple goroutines and processes.
31+
type Queue struct {
32+
db *database.DB
33+
}
34+
35+
// New creates the queue_tasks table if it doesn't exist and returns a Queue.
36+
func New(ctx context.Context, db *database.DB) (*Queue, error) {
37+
// TODO(jbarkhuysen): If we find it onerous to do table updates over time, we
38+
// may want to consider alternatives to doing this here.
39+
if _, err := db.Exec(ctx, createTableQuery); err != nil {
40+
return nil, fmt.Errorf("pgqueue.New: creating table: %w", err)
41+
}
42+
return &Queue{db: db}, nil
43+
}
44+
45+
const createTableQuery = `
46+
CREATE TABLE IF NOT EXISTS queue_tasks (
47+
id BIGSERIAL PRIMARY KEY,
48+
task_name TEXT UNIQUE NOT NULL,
49+
module_path TEXT NOT NULL,
50+
version TEXT NOT NULL,
51+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
52+
started_at TIMESTAMPTZ
53+
);
54+
CREATE INDEX IF NOT EXISTS idx_queue_tasks_started_created
55+
ON queue_tasks (started_at, created_at);`
56+
57+
// ScheduleFetch inserts a task into queue_tasks. It returns (true, nil) if the
58+
// task was inserted, or (false, nil) if it was a duplicate.
59+
func (q *Queue) ScheduleFetch(ctx context.Context, modulePath, version string, opts *queue.Options) (bool, error) {
60+
taskName := modulePath + "@" + version
61+
if opts != nil && opts.Suffix != "" {
62+
taskName += "-" + opts.Suffix
63+
}
64+
n, err := q.db.Exec(ctx,
65+
`INSERT INTO queue_tasks (task_name, module_path, version) VALUES ($1, $2, $3) ON CONFLICT (task_name) DO NOTHING`,
66+
taskName, modulePath, version)
67+
if err != nil {
68+
return false, fmt.Errorf("pgqueue.ScheduleFetch(%q, %q): %w", modulePath, version, err)
69+
}
70+
return n == 1, nil
71+
}
72+
73+
// Poll starts background polling for work. It spawns the given number of worker
74+
// goroutines, each of which periodically claims a task, runs processFunc, and
75+
// deletes the task on completion. It blocks until ctx is cancelled.
76+
func (q *Queue) Poll(ctx context.Context, workers int, processFunc ProcessFunc) {
77+
wg := sync.WaitGroup{}
78+
for range workers {
79+
wg.Go(func() {
80+
// Periodically claim work.
81+
ticker := time.NewTicker(pollInterval)
82+
defer ticker.Stop()
83+
for {
84+
select {
85+
case <-ctx.Done():
86+
return
87+
case <-ticker.C:
88+
q.claimAndProcess(ctx, processFunc)
89+
}
90+
}
91+
})
92+
}
93+
wg.Wait()
94+
}
95+
96+
// TODO(jbarkhuysen): 5m stall timeout is baked in; we might want to make it
97+
// variable in the future.
98+
const dequeueQuery = `
99+
WITH next_task AS (
100+
SELECT id
101+
FROM queue_tasks
102+
WHERE started_at IS NULL
103+
OR started_at + INTERVAL '5 minutes' < NOW()
104+
ORDER BY created_at ASC
105+
LIMIT 1
106+
FOR UPDATE SKIP LOCKED
107+
)
108+
UPDATE queue_tasks
109+
SET started_at = NOW()
110+
WHERE id = (SELECT id FROM next_task)
111+
RETURNING id, module_path, version, started_at`
112+
113+
func (q *Queue) claimAndProcess(ctx context.Context, processFunc ProcessFunc) {
114+
var id int64
115+
var modulePath, version string
116+
var startedAt time.Time
117+
err := q.db.QueryRow(ctx, dequeueQuery).Scan(&id, &modulePath, &version, &startedAt)
118+
if errors.Is(err, sql.ErrNoRows) {
119+
return // There's no work: no-op.
120+
}
121+
if err != nil {
122+
log.Errorf(ctx, "pgqueue: dequeue: %v", err)
123+
return
124+
}
125+
126+
log.Infof(ctx, "pgqueue: processing %s@%s (task %d)", modulePath, version, id)
127+
code, err := processFunc(ctx, modulePath, version)
128+
if err != nil {
129+
log.Errorf(ctx, "pgqueue: processing %s@%s: status=%d err=%v", modulePath, version, code, err)
130+
// This still gets removed (delete below) so that we don't endlessly
131+
// fail the same work item.
132+
}
133+
134+
// Use a background context for cleanup so the delete succeeds even if
135+
// the poll context has been cancelled.
136+
delCtx := context.Background()
137+
if _, err := q.db.Exec(delCtx, `DELETE FROM queue_tasks WHERE id = $1 AND started_at = $2`, id, startedAt); err != nil {
138+
log.Errorf(delCtx, "pgqueue: deleting task %d: %v", id, err)
139+
}
140+
}

0 commit comments

Comments
 (0)