Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add a Pro-only dead letter queue panel that lists discarded jobs newest first, with ID, kind, queue, priority, and tag filters, dead letter autocomplete, and More/Fewer controls to inspect jobs and retry them as new jobs. [PR #711](https://github.com/riverqueue/riverui/pull/711).

### Changed

- Require Node.js 24.15+ or 26+ for frontend development and use Node.js 24.21.0 in CI and container builds to support the updated test dependencies. [PR #690](https://github.com/riverqueue/riverui/pull/690).

### Fixed

- Show a "Page not found" page with a link back to Jobs for unknown URLs, instead of a blank page. [PR #711](https://github.com/riverqueue/riverui/pull/711).
- Keep a long job kind on one line in the job detail heading, with a tooltip and a copy button for the full kind. [PR #711](https://github.com/riverqueue/riverui/pull/711).
- Cancel job search suggestion requests that a newer keystroke has replaced. [PR #711](https://github.com/riverqueue/riverui/pull/711).
- Clear filters and pagination when switching between Jobs and Dead Letters, while preserving them when switching job states within Jobs. [PR #711](https://github.com/riverqueue/riverui/pull/711).
- Show queues whose concurrency metadata is missing, `null`, or `{}` as having no concurrency override, matching how River Pro interprets them, and stop showing a zero concurrency config for producers that report none. [PR #704](https://github.com/riverqueue/riverui/pull/704).
- Reject negative queue concurrency `global_limit` and `local_limit` values with a 400 error instead of storing them. [PR #704](https://github.com/riverqueue/riverui/pull/704).
- Keep workflow diagram layouts compatible with Dagre's package exports after dependency upgrades. [PR #687](https://github.com/riverqueue/riverui/pull/687).
Expand Down
112 changes: 112 additions & 0 deletions internal/riveruidriver/river_ui_driver_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
package riveruidriver

import (
"cmp"
"context"
"errors"
"fmt"
"slices"
"time"

"github.com/riverqueue/river/riverdriver"
Expand All @@ -28,6 +32,114 @@ type Executor interface {
// JobCountEstimate returns inexpensive database estimates where supported.
// Drivers without an estimate strategy return an empty map.
JobCountEstimate(ctx context.Context, params *JobCountEstimateParams) (map[rivertype.JobState]JobCountEstimateResult, error)
// JobDeadLetterList returns at most Max jobs, ordered by ID descending so
// the newest dead letter jobs come first and stay reachable when more jobs
// match than Max. Only Pro endpoints call this query; it requires the dead
// letter table.
JobDeadLetterList(ctx context.Context, params *JobDeadLetterListParams) ([]*JobDeadLetterListResult, error)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Major (design): these Pro-only queries are in the OSS driver.
JobDeadLetterList and JobDeadLetterAutocomplete are on the OSS Executor interface, with a stub schema for a table that River Pro owns. Only Pro endpoints call them. That is why the Pro handler must select an OSS UI driver for itself.
The Pro driver already has JobDeadLetterGetByID and JobDeadLetterGetAll. A filtered, limited list and the autocomplete query fit there. The Pro handler can then call a.DB as it does for get and retry, and the OSS interface loses both methods and the stub table.
This needs a River Pro release first. If you keep the queries here for now, choose the UI driver once when the handler is built and put it on APIBundle, so that no endpoint selects its own.

// JobDeadLetterAutocomplete returns at most Max distinct values of the
// facet that start with Match and aren't in Exclude. Every driver compares
// and orders values the same way regardless of database collation: Match
// ignores the case of ASCII letters only, and values sort by their UTF-8
// bytes. Only Pro endpoints call this query; it requires the dead letter
// table.
JobDeadLetterAutocomplete(ctx context.Context, params *JobDeadLetterAutocompleteParams) ([]string, error)
}

const (
// JobDeadLetterListMaxDefault is the number of jobs to list when the caller
// has no preference.
JobDeadLetterListMaxDefault = 20
// JobDeadLetterListMaxLimit is the largest Max that JobDeadLetterList
// accepts.
JobDeadLetterListMaxLimit = 200
// JobDeadLetterAutocompleteMaxLimit is the largest Max that
// JobDeadLetterAutocomplete accepts.
JobDeadLetterAutocompleteMaxLimit = 100
)

// JobDeadLetterFacet is a job attribute that JobDeadLetterAutocomplete can
// suggest values for. The queries compare against these values literally.
type JobDeadLetterFacet string

const (
JobDeadLetterFacetJobKind JobDeadLetterFacet = "job_kind"
JobDeadLetterFacetQueueName JobDeadLetterFacet = "queue_name"
)

// JobDeadLetterFilters matches any value within each populated filter and
// requires all populated filters to match. Tags match any exact tag.
type JobDeadLetterFilters struct {
IDs []int64 `json:"ids,omitempty"`
Kinds []string `json:"kinds,omitempty"`
Priorities []int16 `json:"priorities,omitempty"`
Queues []string `json:"queues,omitempty"`
Tags []string `json:"tags,omitempty"`
}

type JobDeadLetterAutocompleteParams struct {
Exclude []string
Facet JobDeadLetterFacet
Match string
Max int
Schema string
}

// Validate reports whether the params are ones that every driver accepts.
// Drivers call it before querying so they all reject the same inputs.
func (p *JobDeadLetterAutocompleteParams) Validate() error {
switch p.Facet {
case JobDeadLetterFacetJobKind, JobDeadLetterFacetQueueName:
default:
return errors.New("invalid dead letter autocomplete facet")
}
if p.Max < 1 || p.Max > JobDeadLetterAutocompleteMaxLimit {
return fmt.Errorf("dead letter autocomplete max must be between 1 and %d", JobDeadLetterAutocompleteMaxLimit)
}
return nil
}

type JobDeadLetterListParams struct {
Filters JobDeadLetterFilters
Max int
Schema string
}

// Validate reports whether the params are ones that every driver accepts.
// Drivers call it before querying so they all reject the same inputs.
func (p *JobDeadLetterListParams) Validate() error {
if p.Max < 1 || p.Max > JobDeadLetterListMaxLimit {
return fmt.Errorf("dead letter list max must be between 1 and %d", JobDeadLetterListMaxLimit)
}
return nil
}

// JobDeadLetterListResult contains only fields displayed in the list. Args,
// metadata, and attempt history are loaded separately when opening a job.
//
// FinalizedAt is in UTC. Its precision is the one difference between drivers:
// PostgreSQL returns microseconds while SQLite returns milliseconds.
type JobDeadLetterListResult struct {
ID int64 `json:"id"`
Attempt int `json:"attempt"`
FinalizedAt *time.Time `json:"finalized_at"`
Kind string `json:"kind"`
MaxAttempts int `json:"max_attempts"`
Queue string `json:"queue"`
}

// NormalizeJobDeadLetterListResults puts decoded jobs into the form that every
// driver returns. Jobs are ordered by ID descending, because not every database
// guarantees the order of elements within an aggregated JSON array. Timestamps
// are in UTC, because PostgreSQL renders them in the session's time zone while
// SQLite renders them in UTC.
func NormalizeJobDeadLetterListResults(jobs []*JobDeadLetterListResult) {
for _, job := range jobs {
if job.FinalizedAt != nil {
*job.FinalizedAt = job.FinalizedAt.UTC()
}
}
slices.SortFunc(jobs, func(a, b *JobDeadLetterListResult) int { return cmp.Compare(b.ID, a.ID) })
}

type JobCountByAllStatesCappedParams struct {
Expand Down
Loading
Loading