Skip to content

Commit ba48132

Browse files
authored
Refactor feature flag definitions, add durable periodic jobs UI (#435)
* dynamic feature flag extensions injected by pro bundle Move all Pro-related flags into the extensions setup so there's no need to check for these Pro artifacts in OSS. Also add a flag for durable periodic jobs. * add periodic job endpoint * fix concurrency settings w/ a single paused producer Also fix Queue stories that depend on feature flags. * implement periodic job list view * remove forked riverinternaltest in favor of shared variants Also refactor to leverage shared DB test TX helpers and parallelize endpoint detection tests which modify the schema (by disbling sharing on those tests).
1 parent 0421e5e commit ba48132

41 files changed

Lines changed: 1518 additions & 429 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/preview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const withFeatures: Decorator = (StoryFn, context) => {
2626
// Default features with story-specific overrides
2727
const features = {
2828
hasProducerTable: true,
29+
producerQueries: true,
2930
...context.parameters?.features,
3031
};
3132

common_test.go

Lines changed: 0 additions & 32 deletions
This file was deleted.

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ toolchain go1.24.4
77
require (
88
github.com/jackc/pgx/v5 v5.7.6
99
github.com/riverqueue/apiframe v0.0.0-20250408034821-b206bbbd0fb4
10-
github.com/riverqueue/river v0.25.0
11-
github.com/riverqueue/river/riverdriver v0.25.0
12-
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.0
13-
github.com/riverqueue/river/rivershared v0.25.0
14-
github.com/riverqueue/river/rivertype v0.25.0
10+
github.com/riverqueue/river v0.25.1-0.20251001013213-822d5bb676cc
11+
github.com/riverqueue/river/riverdriver v0.25.1-0.20251001013213-822d5bb676cc
12+
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.1-0.20251001013213-822d5bb676cc
13+
github.com/riverqueue/river/rivershared v0.25.1-0.20251001013213-822d5bb676cc
14+
github.com/riverqueue/river/rivertype v0.25.1-0.20251001013213-822d5bb676cc
1515
github.com/rs/cors v1.11.1
1616
github.com/samber/slog-http v1.8.2
1717
github.com/stretchr/testify v1.11.1

go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
3535
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3636
github.com/riverqueue/apiframe v0.0.0-20250408034821-b206bbbd0fb4 h1:ejJogJ57bF+jMbvGjZQ6H6LR0NCTDQr30SJ/wSVepgs=
3737
github.com/riverqueue/apiframe v0.0.0-20250408034821-b206bbbd0fb4/go.mod h1:6aXA9FSXKkxwjbOUSXdrIOuw478Lvtz/eEu45R4MoQk=
38-
github.com/riverqueue/river v0.25.0 h1:dRnA9ltq9hTYRMmZgBnhqRh3AzBIFVu+qVLpBqy6b+g=
39-
github.com/riverqueue/river v0.25.0/go.mod h1:KetN5MQQu9IjtganQrIt0OFubweeh+qkAqJaCdalwtI=
40-
github.com/riverqueue/river/riverdriver v0.25.0 h1:RkvBWBlybYGaU1DoQ/mSwnWp1hm0FfS8yyksr/dM5tI=
41-
github.com/riverqueue/river/riverdriver v0.25.0/go.mod h1:p2Jvr1N6NfPA+ngIKK8urqxG2vmusX4jO7g/UH/soQY=
42-
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.0 h1:Ed6dtSSwsj7VwbquG6Bh+2+271sBOL6WyRbisY/XHiY=
43-
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.0/go.mod h1:h77bWaGJyA5GMKEKmANQN9mhsV3XWYt4sRUx6FtQa84=
44-
github.com/riverqueue/river/rivershared v0.25.0 h1:grjuTHJEVvi4srzcspQ2UXWjISxdqbubQl+9DDg3agQ=
45-
github.com/riverqueue/river/rivershared v0.25.0/go.mod h1:ZdVeOnT8X8PiAZRUfWHc+Ne6fNXqe1oYb2eioZb6URM=
46-
github.com/riverqueue/river/rivertype v0.25.0 h1:DPwd0DGqajLIv9zsB+BOwlum0D1/4Iiqz34+nwIZaZ0=
47-
github.com/riverqueue/river/rivertype v0.25.0/go.mod h1:9bbWVYkr1B/YzW43lUs/Vk/tEYqLrabrZWrtUWQ+Goo=
38+
github.com/riverqueue/river v0.25.1-0.20251001013213-822d5bb676cc h1:G9TV3+iiEMs18MAeexA8gb4KltmHNvIE1uB4ra6k6yY=
39+
github.com/riverqueue/river v0.25.1-0.20251001013213-822d5bb676cc/go.mod h1:KetN5MQQu9IjtganQrIt0OFubweeh+qkAqJaCdalwtI=
40+
github.com/riverqueue/river/riverdriver v0.25.1-0.20251001013213-822d5bb676cc h1:AAMdacQhDBNebXmjuiGaPHeZAoZT77ELelhIMoJLD7E=
41+
github.com/riverqueue/river/riverdriver v0.25.1-0.20251001013213-822d5bb676cc/go.mod h1:p2Jvr1N6NfPA+ngIKK8urqxG2vmusX4jO7g/UH/soQY=
42+
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.1-0.20251001013213-822d5bb676cc h1:uOZcH6W9ItGhG4IFziy54Zkbo9O7t4BDELyCnCjX1y4=
43+
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.1-0.20251001013213-822d5bb676cc/go.mod h1:h77bWaGJyA5GMKEKmANQN9mhsV3XWYt4sRUx6FtQa84=
44+
github.com/riverqueue/river/rivershared v0.25.1-0.20251001013213-822d5bb676cc h1:7KjQiHOdPioVtGLwO4imQvt7swiGeww1oF4JZfcEaLk=
45+
github.com/riverqueue/river/rivershared v0.25.1-0.20251001013213-822d5bb676cc/go.mod h1:ZdVeOnT8X8PiAZRUfWHc+Ne6fNXqe1oYb2eioZb6URM=
46+
github.com/riverqueue/river/rivertype v0.25.1-0.20251001013213-822d5bb676cc h1:75lj6WkDnCNaF6HJl3PKDQj+TlDypmvfGl9b28uLghA=
47+
github.com/riverqueue/river/rivertype v0.25.1-0.20251001013213-822d5bb676cc/go.mod h1:9bbWVYkr1B/YzW43lUs/Vk/tEYqLrabrZWrtUWQ+Goo=
4848
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
4949
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
5050
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=

handler.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ import (
2828
"riverqueue.com/riverui/uiendpoints"
2929
)
3030

31-
type endpointsExtensions interface {
32-
Extensions() map[string]bool
33-
}
34-
3531
// EndpointsOpts are the options for creating a new Endpoints bundle.
3632
type EndpointsOpts[TTx any] struct {
3733
// Tx is an optional transaction to wrap all database operations. It's mainly
@@ -42,6 +38,7 @@ type EndpointsOpts[TTx any] struct {
4238
type endpoints[TTx any] struct {
4339
bundleOpts *uiendpoints.BundleOpts
4440
client *river.Client[TTx]
41+
extensions func(ctx context.Context) (map[string]bool, error)
4542
opts *EndpointsOpts[TTx]
4643
}
4744

@@ -66,14 +63,27 @@ func (e *endpoints[TTx]) Configure(bundleOpts *uiendpoints.BundleOpts) {
6663
e.bundleOpts = bundleOpts
6764
}
6865

66+
func (e *endpoints[TTx]) Extensions(ctx context.Context) (map[string]bool, error) {
67+
if e.extensions == nil {
68+
return map[string]bool{}, nil
69+
}
70+
return e.extensions(ctx)
71+
}
72+
73+
// SetExtensionsProvider sets the extensions provider function for this bundle.
74+
// This is a private "friend interface" method for use by riverproui.
75+
func (e *endpoints[TTx]) SetExtensionsProvider(provider func(ctx context.Context) (map[string]bool, error)) {
76+
e.extensions = provider
77+
}
78+
6979
func (e *endpoints[TTx]) Validate() error {
7080
if e.client == nil {
7181
return errors.New("client is required")
7282
}
7383
return nil
7484
}
7585

76-
func (e *endpoints[TTx]) MountEndpoints(archetype *baseservice.Archetype, logger *slog.Logger, mux *http.ServeMux, mountOpts *apiendpoint.MountOpts, extensions map[string]bool) []apiendpoint.EndpointInterface {
86+
func (e *endpoints[TTx]) MountEndpoints(archetype *baseservice.Archetype, logger *slog.Logger, mux *http.ServeMux, mountOpts *apiendpoint.MountOpts) []apiendpoint.EndpointInterface {
7787
driver := e.client.Driver()
7888
var executor riverdriver.Executor
7989
if e.opts.Tx == nil {
@@ -86,7 +96,7 @@ func (e *endpoints[TTx]) MountEndpoints(archetype *baseservice.Archetype, logger
8696
Client: e.client,
8797
DB: executor,
8898
Driver: driver,
89-
Extensions: extensions,
99+
Extensions: e.Extensions,
90100
JobListHideArgsByDefault: e.bundleOpts.JobListHideArgsByDefault,
91101
Logger: logger,
92102
}
@@ -232,12 +242,7 @@ func NewHandler(opts *HandlerOpts) (*Handler, error) {
232242
Validator: apitype.NewValidator(),
233243
}
234244

235-
extensions := map[string]bool{}
236-
if withExtensions, ok := opts.Endpoints.(endpointsExtensions); ok {
237-
extensions = withExtensions.Extensions()
238-
}
239-
240-
endpoints := opts.Endpoints.MountEndpoints(baseservice.NewArchetype(opts.Logger), opts.Logger, mux, &mountOpts, extensions)
245+
endpoints := opts.Endpoints.MountEndpoints(baseservice.NewArchetype(opts.Logger), opts.Logger, mux, &mountOpts)
241246

242247
var services []startstop.Service
243248

handler_api_endpoint.go

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -176,62 +176,19 @@ type featuresGetRequest struct{}
176176

177177
type featuresGetResponse struct {
178178
Extensions map[string]bool `json:"extensions"`
179-
HasClientTable bool `json:"has_client_table"`
180-
HasProducerTable bool `json:"has_producer_table"`
181-
HasSequenceTable bool `json:"has_sequence_table"`
182-
HasWorkflows bool `json:"has_workflows"`
183179
JobListHideArgsByDefault bool `json:"job_list_hide_args_by_default"`
184180
}
185181

186182
func (a *featuresGetEndpoint[TTx]) Execute(ctx context.Context, _ *featuresGetRequest) (*featuresGetResponse, error) {
187-
return dbutil.WithTxV(ctx, a.DB, func(ctx context.Context, execTx riverdriver.ExecutorTx) (*featuresGetResponse, error) {
188-
tx := a.Driver.UnwrapTx(execTx)
189-
190-
schema := a.Client.Schema()
191-
hasClientTable, err := a.Driver.UnwrapExecutor(tx).TableExists(ctx, &riverdriver.TableExistsParams{
192-
Schema: schema,
193-
Table: "river_client",
194-
})
195-
if err != nil {
196-
return nil, err
197-
}
198-
199-
hasProducerTable, err := a.Driver.UnwrapExecutor(tx).TableExists(ctx, &riverdriver.TableExistsParams{
200-
Schema: schema,
201-
Table: "river_producer",
202-
})
203-
if err != nil {
204-
return nil, err
205-
}
206-
207-
hasSequenceTable, err := a.Driver.UnwrapExecutor(tx).TableExists(ctx, &riverdriver.TableExistsParams{
208-
Schema: schema,
209-
Table: "river_job_sequence",
210-
})
211-
if err != nil {
212-
return nil, err
213-
}
214-
215-
indexResults, err := a.Driver.UnwrapExecutor(tx).IndexesExist(ctx, &riverdriver.IndexesExistParams{
216-
IndexNames: []string{
217-
"river_job_workflow_list_active",
218-
"river_job_workflow_scheduling",
219-
},
220-
Schema: schema,
221-
})
222-
if err != nil {
223-
return nil, err
224-
}
183+
extensions, err := a.Extensions(ctx)
184+
if err != nil {
185+
return nil, err
186+
}
225187

226-
return &featuresGetResponse{
227-
Extensions: a.Extensions,
228-
HasClientTable: hasClientTable,
229-
HasProducerTable: hasProducerTable,
230-
HasSequenceTable: hasSequenceTable,
231-
HasWorkflows: indexResults["river_job_workflow_list_active"] || indexResults["river_job_workflow_scheduling"],
232-
JobListHideArgsByDefault: a.JobListHideArgsByDefault,
233-
}, nil
234-
})
188+
return &featuresGetResponse{
189+
Extensions: extensions,
190+
JobListHideArgsByDefault: a.JobListHideArgsByDefault,
191+
}, nil
235192
}
236193

237194
//

handler_api_endpoint_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import (
1414
"github.com/riverqueue/apiframe/apitest"
1515
"github.com/riverqueue/apiframe/apitype"
1616
"github.com/riverqueue/river"
17+
"github.com/riverqueue/river/riverdbtest"
1718
"github.com/riverqueue/river/riverdriver"
19+
"github.com/riverqueue/river/riverdriver/riverpgxv5"
1820
"github.com/riverqueue/river/rivershared/riversharedtest"
1921
"github.com/riverqueue/river/rivershared/startstop"
2022
"github.com/riverqueue/river/rivershared/util/ptrutil"
2123
"github.com/riverqueue/river/rivertype"
2224

2325
"riverqueue.com/riverui/internal/apibundle"
24-
"riverqueue.com/riverui/internal/riverinternaltest"
2526
"riverqueue.com/riverui/internal/riverinternaltest/testfactory"
2627
"riverqueue.com/riverui/internal/uicommontest"
2728
)
@@ -37,18 +38,23 @@ func setupEndpoint[TEndpoint any](ctx context.Context, t *testing.T, initFunc fu
3738
t.Helper()
3839

3940
var (
40-
logger = riverinternaltest.Logger(t)
41-
client, driver = insertOnlyClient(t, logger)
42-
tx = riverinternaltest.TestTx(ctx, t)
43-
exec = driver.UnwrapExecutor(tx)
41+
logger = riversharedtest.Logger(t)
42+
driver = riverpgxv5.New(riversharedtest.DBPool(ctx, t))
43+
tx, _ = riverdbtest.TestTxPgxDriver(ctx, t, driver, nil)
44+
exec = driver.UnwrapExecutor(tx)
4445
)
4546

47+
client, err := river.NewClient(driver, &river.Config{
48+
Logger: logger,
49+
})
50+
require.NoError(t, err)
51+
4652
endpoint := initFunc(apibundle.APIBundle[pgx.Tx]{
4753
Archetype: riversharedtest.BaseServiceArchetype(t),
4854
Client: client,
4955
DB: exec,
5056
Driver: driver,
51-
Extensions: map[string]bool{},
57+
Extensions: func(_ context.Context) (map[string]bool, error) { return map[string]bool{}, nil },
5258
Logger: logger,
5359
})
5460

@@ -68,7 +74,7 @@ func setupEndpoint[TEndpoint any](ctx context.Context, t *testing.T, initFunc fu
6874
func testMountOpts(t *testing.T) *apiendpoint.MountOpts {
6975
t.Helper()
7076
return &apiendpoint.MountOpts{
71-
Logger: riverinternaltest.Logger(t),
77+
Logger: riversharedtest.Logger(t),
7278
Validator: apitype.NewValidator(),
7379
}
7480
}
@@ -247,10 +253,6 @@ func TestAPIHandlerFeaturesGet(t *testing.T) {
247253
require.NoError(t, err)
248254
require.Equal(t, &featuresGetResponse{
249255
Extensions: map[string]bool{},
250-
HasClientTable: false,
251-
HasProducerTable: false,
252-
HasSequenceTable: false,
253-
HasWorkflows: false,
254256
JobListHideArgsByDefault: false,
255257
}, resp)
256258
})
@@ -276,10 +278,6 @@ func TestAPIHandlerFeaturesGet(t *testing.T) {
276278
require.NoError(t, err)
277279
require.Equal(t, &featuresGetResponse{
278280
Extensions: map[string]bool{},
279-
HasClientTable: true,
280-
HasProducerTable: true,
281-
HasSequenceTable: true,
282-
HasWorkflows: true,
283281
JobListHideArgsByDefault: true,
284282
}, resp)
285283
})
@@ -288,9 +286,11 @@ func TestAPIHandlerFeaturesGet(t *testing.T) {
288286
t.Parallel()
289287

290288
endpoint, _ := setupEndpoint(ctx, t, newFeaturesGetEndpoint)
291-
endpoint.Extensions = map[string]bool{
292-
"test_1": true,
293-
"test_2": false,
289+
endpoint.Extensions = func(_ context.Context) (map[string]bool, error) {
290+
return map[string]bool{
291+
"test_1": true,
292+
"test_2": false,
293+
}, nil
294294
}
295295

296296
resp, err := apitest.InvokeHandler(ctx, endpoint.Execute, testMountOpts(t), &featuresGetRequest{})

handler_test.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package riverui
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67
"net/http"
78
"net/http/httptest"
89
"testing"
@@ -12,10 +13,12 @@ import (
1213

1314
"github.com/riverqueue/apiframe/apitype"
1415
"github.com/riverqueue/river"
16+
"github.com/riverqueue/river/riverdbtest"
1517
"github.com/riverqueue/river/riverdriver"
18+
"github.com/riverqueue/river/riverdriver/riverpgxv5"
19+
"github.com/riverqueue/river/rivershared/riversharedtest"
1620

1721
"riverqueue.com/riverui/internal/handlertest"
18-
"riverqueue.com/riverui/internal/riverinternaltest"
1922
"riverqueue.com/riverui/internal/riverinternaltest/testfactory"
2023
"riverqueue.com/riverui/internal/uicommontest"
2124
"riverqueue.com/riverui/uiendpoints"
@@ -24,7 +27,19 @@ import (
2427
func TestNewHandlerIntegration(t *testing.T) {
2528
t.Parallel()
2629

27-
createClient := insertOnlyClient
30+
createClient := func(ctx context.Context, tb testing.TB, logger *slog.Logger) (*river.Client[pgx.Tx], riverdriver.Driver[pgx.Tx], pgx.Tx) {
31+
tb.Helper()
32+
33+
driver := riverpgxv5.New(riversharedtest.DBPool(ctx, tb))
34+
tx, _ := riverdbtest.TestTxPgxDriver(ctx, tb, driver, nil)
35+
36+
client, err := river.NewClient(driver, &river.Config{
37+
Logger: logger,
38+
})
39+
require.NoError(tb, err)
40+
41+
return client, driver, tx
42+
}
2843

2944
createBundle := func(client *river.Client[pgx.Tx], tx pgx.Tx) uiendpoints.Bundle {
3045
return NewEndpoints(client, &EndpointsOpts[pgx.Tx]{
@@ -35,7 +50,7 @@ func TestNewHandlerIntegration(t *testing.T) {
3550
createHandler := func(t *testing.T, bundle uiendpoints.Bundle) http.Handler {
3651
t.Helper()
3752

38-
logger := riverinternaltest.Logger(t)
53+
logger := riversharedtest.Logger(t)
3954
server, err := NewHandler(&HandlerOpts{
4055
DevMode: true,
4156
Endpoints: bundle,
@@ -105,7 +120,7 @@ func TestMountStaticFiles(t *testing.T) {
105120
t.Parallel()
106121

107122
var (
108-
logger = riverinternaltest.Logger(t)
123+
logger = riversharedtest.Logger(t)
109124
mux = http.NewServeMux()
110125
)
111126

0 commit comments

Comments
 (0)