Skip to content

Commit ad7125b

Browse files
authored
Workflow retry (#430)
* use riverpro v0.18.0, river v0.25.0 * implement workflow retry endpoint * expose workflow retry on frontend * JobTImeline: fix wait duration
1 parent a3e11bb commit ad7125b

15 files changed

Lines changed: 783 additions & 59 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Added the ability to cancel workflows from the workflow detail page. [PR #407](https://github.com/riverqueue/riverui/pull/407).
13+
- Added the ability to retry workflows from the workflow detail page. [PR #430](https://github.com/riverqueue/riverui/pull/430).
1314

1415
### Fixed
1516

1617
- Workflow detail: prevent the viewport from getting stuck zoomed into empty space after navigating between the workflow list and detail pages by remounting the diagram per workflow and enabling fit-to-view on mount. [PR #408](https://github.com/riverqueue/riverui/pull/408)
1718
- Workflow detail: make MiniMap render nodes correctly and respect dark mode by setting explicit node bounds and theme-aware colors. [PR #408](https://github.com/riverqueue/riverui/pull/408)
19+
- Job detail: show correct wait duration for running or completed jobs. [PR #430](https://github.com/riverqueue/riverui/pull/430).
1820

1921
## [v0.12.2] - 2025-08-16
2022

common_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66

77
"github.com/jackc/pgx/v5"
88
"github.com/stretchr/testify/require"
9-
"riverqueue.com/riverui/internal/uicommontest"
109

1110
"github.com/riverqueue/river"
1211
"github.com/riverqueue/river/riverdriver"
1312
"github.com/riverqueue/river/riverdriver/riverpgxv5"
13+
14+
"riverqueue.com/riverui/internal/uicommontest"
1415
)
1516

1617
func insertOnlyClient(t *testing.T, logger *slog.Logger) (*river.Client[pgx.Tx], riverdriver.Driver[pgx.Tx]) {

int64_string_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/require"
10+
1011
"riverqueue.com/riverui/internal/uicommontest"
1112
)
1213

riverproui/endpoints.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func (e *endpoints[TTx]) MountEndpoints(archetype *baseservice.Archetype, logger
9292
apiendpoint.Mount(mux, prohandler.NewWorkflowCancelEndpoint(bundle), mountOpts),
9393
apiendpoint.Mount(mux, prohandler.NewWorkflowGetEndpoint(bundle), mountOpts),
9494
apiendpoint.Mount(mux, prohandler.NewWorkflowListEndpoint(bundle), mountOpts),
95+
apiendpoint.Mount(mux, prohandler.NewWorkflowRetryEndpoint(bundle), mountOpts),
9596
)
9697

9798
return endpoints

riverproui/go.mod

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
module riverqueue.com/riverui/riverproui
22

3-
go 1.24
3+
go 1.24.0
44

55
toolchain go1.24.4
66

77
require (
88
github.com/google/uuid v1.6.0
99
github.com/jackc/pgx/v5 v5.7.6
1010
github.com/riverqueue/apiframe v0.0.0-20250708014637-e55c49c01ff7
11-
github.com/riverqueue/river v0.24.0
12-
github.com/riverqueue/river/riverdriver v0.24.0
13-
github.com/riverqueue/river/rivershared v0.24.0
14-
github.com/riverqueue/river/rivertype v0.24.0
11+
github.com/riverqueue/river v0.25.0
12+
github.com/riverqueue/river/riverdriver v0.25.0
13+
github.com/riverqueue/river/rivershared v0.25.0
14+
github.com/riverqueue/river/rivertype v0.25.0
1515
github.com/stretchr/testify v1.11.1
16-
riverqueue.com/riverpro v0.16.0
17-
riverqueue.com/riverpro/driver v0.16.0
18-
riverqueue.com/riverpro/driver/riverpropgxv5 v0.16.0
16+
riverqueue.com/riverpro v0.18.0
17+
riverqueue.com/riverpro/driver v0.18.0
18+
riverqueue.com/riverpro/driver/riverpropgxv5 v0.18.0
1919
riverqueue.com/riverui v0.12.2
2020
)
2121

@@ -31,21 +31,21 @@ require (
3131
github.com/jackc/puddle/v2 v2.2.2 // indirect
3232
github.com/leodido/go-urn v1.4.0 // indirect
3333
github.com/pmezard/go-difflib v1.0.0 // indirect
34-
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 // indirect
34+
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.25.0 // indirect
3535
github.com/rs/cors v1.11.1 // indirect
3636
github.com/samber/slog-http v1.8.1 // indirect
3737
github.com/tidwall/gjson v1.18.0 // indirect
38-
github.com/tidwall/match v1.1.1 // indirect
38+
github.com/tidwall/match v1.2.0 // indirect
3939
github.com/tidwall/pretty v1.2.1 // indirect
4040
github.com/tidwall/sjson v1.2.5 // indirect
4141
go.opentelemetry.io/otel v1.29.0 // indirect
4242
go.opentelemetry.io/otel/trace v1.29.0 // indirect
4343
go.uber.org/goleak v1.3.0 // indirect
44-
golang.org/x/crypto v0.41.0 // indirect
45-
golang.org/x/net v0.42.0 // indirect
46-
golang.org/x/sync v0.16.0 // indirect
47-
golang.org/x/sys v0.35.0 // indirect
48-
golang.org/x/text v0.28.0 // indirect
44+
golang.org/x/crypto v0.42.0 // indirect
45+
golang.org/x/net v0.43.0 // indirect
46+
golang.org/x/sync v0.17.0 // indirect
47+
golang.org/x/sys v0.36.0 // indirect
48+
golang.org/x/text v0.29.0 // indirect
4949
gopkg.in/yaml.v3 v3.0.1 // indirect
5050
)
5151

riverproui/go.sum

Lines changed: 28 additions & 27 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-20250708014637-e55c49c01ff7 h1:A16RTdTAQ2cIY++FPjiQ9yL8/2FXR4wYmaN7SpP3yP0=
3737
github.com/riverqueue/apiframe v0.0.0-20250708014637-e55c49c01ff7/go.mod h1:jV49jb/qzxSqwDajmG4N2Cm50KATxblxlSiXMF9Ck1E=
38-
github.com/riverqueue/river v0.24.0 h1:CesL6vymWgz0d+zNwtnSGRWaB+E8Dax+o9cxD7sUmKc=
39-
github.com/riverqueue/river v0.24.0/go.mod h1:UZ3AxU5t6WtyqNssaea/AkRS8h/kJ+E9ImSB3xyb3ns=
40-
github.com/riverqueue/river/riverdriver v0.24.0 h1:HqGgGkls11u+YKDA7cKOdYKlQwRNJyHuGa3UtOvpdT0=
41-
github.com/riverqueue/river/riverdriver v0.24.0/go.mod h1:dEew9DDIKenNvzpm8Edw8+PkqP3c0zl1fKjiQTq2n/w=
42-
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 h1:yV37OIbRrhRwIiGeRT7P4D3szhAemu87BgCf8gTCoU4=
43-
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0/go.mod h1:QfznySVKC4ljx53syd/bA/LRSsydAyuD3Q9/EbSniKA=
44-
github.com/riverqueue/river/rivershared v0.24.0 h1:KysokksW75pug2a5RTOc6WESOupWmsylVc6VWvAx+4Y=
45-
github.com/riverqueue/river/rivershared v0.24.0/go.mod h1:UIBfSdai0oWFlwFcoqG4DZX83iA/fLWTEBGrj7Oe1ho=
46-
github.com/riverqueue/river/rivertype v0.24.0 h1:xrQZm/h6U8TBPyTsQPYD5leOapuoBAcdz30bdBwTqOg=
47-
github.com/riverqueue/river/rivertype v0.24.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs=
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=
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=
@@ -61,8 +61,9 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
6161
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
6262
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
6363
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
64-
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
6564
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
65+
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
66+
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
6667
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
6768
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
6869
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -74,25 +75,25 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
7475
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
7576
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
7677
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
77-
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
78-
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
79-
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
80-
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
81-
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
82-
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
83-
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
84-
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
85-
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
86-
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
78+
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
79+
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
80+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
81+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
82+
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
83+
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
84+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
85+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
86+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
87+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
8788
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8889
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
8990
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
9091
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
9192
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
9293
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
93-
riverqueue.com/riverpro v0.16.0 h1:y6w5TaC9gNkIFDZJgDbrnaeiqo5weTSAgWOvHGsoDdk=
94-
riverqueue.com/riverpro v0.16.0/go.mod h1:d70DhPfPjUKy+SkDmPUkU0vuOtLWxNmB4T+sE2w2/Jo=
95-
riverqueue.com/riverpro/driver v0.16.0 h1:yhFnXcX9PDCu3chGuLLR6Jx3EuPYUoK20gFv31DR6c4=
96-
riverqueue.com/riverpro/driver v0.16.0/go.mod h1:MrbBVCcEQWWKeQiDMJEXAP6fuEyN1UqQ5CXd+m4rWrQ=
97-
riverqueue.com/riverpro/driver/riverpropgxv5 v0.16.0 h1:iC+9a7mVVaJH/El4Z/MmzFaxMoIAn+QGEDlFLST0gVs=
98-
riverqueue.com/riverpro/driver/riverpropgxv5 v0.16.0/go.mod h1:TKQGLJyfOwCq133ttA4i8TI90uF4C4zTqEArAHOD37g=
94+
riverqueue.com/riverpro v0.18.0 h1:cGIU7rIeAS2OYRVN5fTtf0SuSwg0AldlGjFdGJGtCpo=
95+
riverqueue.com/riverpro v0.18.0/go.mod h1:7FmhsAcTaL8pwJtIN17aU5VjAnP46zJ3+9RrJvx4pDQ=
96+
riverqueue.com/riverpro/driver v0.18.0 h1:+UxiZn++M9dNX5iDFNPqEQ+67QOA2cqXF7IgpCruR2E=
97+
riverqueue.com/riverpro/driver v0.18.0/go.mod h1:T1G/XM9H/Ccs5FaHICpVXWP0+qN4m8+hruyLRgorKzU=
98+
riverqueue.com/riverpro/driver/riverpropgxv5 v0.18.0 h1:vFuinoXIVetyK2bAs8Dom0+uW2O0vJ0sMDnx8yFQXJI=
99+
riverqueue.com/riverpro/driver/riverpropgxv5 v0.18.0/go.mod h1:oGVzU7TAG9clY9IykAbVE1eugXS+YKh5Pk29K8bvstQ=

riverproui/internal/prohandler/pro_handler_api_endpoints.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,77 @@ func (a *workflowListEndpoint[TTx]) Execute(ctx context.Context, req *workflowLi
291291
}
292292
}
293293

294+
//
295+
// workflowRetryEndpoint
296+
//
297+
298+
type workflowRetryEndpoint[TTx any] struct {
299+
ProAPIBundle[TTx]
300+
apiendpoint.Endpoint[workflowRetryRequest, workflowRetryResponse]
301+
}
302+
303+
func NewWorkflowRetryEndpoint[TTx any](apiBundle ProAPIBundle[TTx]) *workflowRetryEndpoint[TTx] {
304+
return &workflowRetryEndpoint[TTx]{ProAPIBundle: apiBundle}
305+
}
306+
307+
func (*workflowRetryEndpoint[TTx]) Meta() *apiendpoint.EndpointMeta {
308+
return &apiendpoint.EndpointMeta{
309+
Pattern: "POST /api/pro/workflows/{id}/retry",
310+
StatusCode: http.StatusOK,
311+
}
312+
}
313+
314+
type workflowRetryRequest struct {
315+
ID string `json:"-" validate:"required"` // from ExtractRaw
316+
Mode string `json:"mode" validate:"omitempty,oneof=all failed_only failed_and_downstream"`
317+
ResetHistory bool `json:"reset_history"`
318+
}
319+
320+
func (req *workflowRetryRequest) ExtractRaw(r *http.Request) error {
321+
req.ID = r.PathValue("id")
322+
return nil
323+
}
324+
325+
type workflowRetryResponse struct {
326+
RetriedJobs []*riverJobMinimal `json:"retried_jobs"`
327+
}
328+
329+
func (a *workflowRetryEndpoint[TTx]) Execute(ctx context.Context, req *workflowRetryRequest) (*workflowRetryResponse, error) {
330+
return dbutil.WithTxV(ctx, a.DB, func(ctx context.Context, execTx riverdriver.ExecutorTx) (*workflowRetryResponse, error) {
331+
tx := a.Driver.UnwrapTx(execTx)
332+
333+
// Build workflow wrapper from existing workflow ID
334+
workflow := a.Client.NewWorkflow(&riverpro.WorkflowOpts{ID: req.ID})
335+
336+
// Determine retry mode (defaults to "all")
337+
var mode riverpro.WorkflowRetryMode
338+
switch req.Mode {
339+
case "failed_only":
340+
mode = riverpro.WorkflowRetryModeFailedOnly
341+
case "failed_and_downstream":
342+
mode = riverpro.WorkflowRetryModeFailedAndDownstream
343+
case "", "all":
344+
mode = riverpro.WorkflowRetryModeAll
345+
default:
346+
// validator should prevent this path; keep safe default
347+
mode = riverpro.WorkflowRetryModeAll
348+
}
349+
350+
result, err := workflow.RetryTx(ctx, tx, &riverpro.WorkflowRetryOpts{
351+
Mode: mode,
352+
ResetHistory: req.ResetHistory,
353+
})
354+
if err != nil {
355+
return nil, err
356+
}
357+
358+
// consistent ordering
359+
slices.SortFunc(result.Jobs, func(a, b *rivertype.JobRow) int { return int(a.ID - b.ID) })
360+
361+
return &workflowRetryResponse{RetriedJobs: sliceutil.Map(result.Jobs, internalJobToJobMinimal)}, nil
362+
})
363+
}
364+
294365
type riverJobMinimal struct {
295366
ID int64 `json:"id"`
296367
Args json.RawMessage `json:"args"`

0 commit comments

Comments
 (0)