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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
cache: true

- name: Test PostgreSQL persistence contracts
run: go test ./internal/db/migrations ./internal/db/repository -count=1
run: go test ./internal/db/migrations ./internal/db/repository ./internal/task -count=1

check:
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
Expand Down
64 changes: 54 additions & 10 deletions cmd/synaps3/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,28 +446,45 @@ func adminTaskCommand() *cli.Command {
},
{
Name: "acknowledge",
Usage: "dismiss a failed task",
ArgsUsage: "<id>",
Usage: "dismiss a failed task, or a backlog of failed tasks",
ArgsUsage: "[id]",
Flags: []cli.Flag{
&cli.StringFlag{Name: "type", Usage: "dismiss only failures of this operation type"},
&cli.StringFlag{Name: "before", Usage: "dismiss only failures recorded before this RFC 3339 time (default: now)"},
&cli.BoolFlag{Name: "yes", Usage: "confirm dismissing every matching failed task"},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
taskID, err := requireSingleArg(cmd, "task id")
if err != nil {
return err
if cmd.Args().Len() > 0 {
if cmd.String("type") != "" || cmd.String("before") != "" || cmd.Bool("yes") {
return errors.New("pass either a task id or the bulk flags, not both")
}
return acknowledgeSingleTask(ctx, cmd)
}
if _, err := strconv.ParseInt(taskID, 10, 64); err != nil {
return fmt.Errorf("invalid task id %q", taskID)
if !cmd.Bool("yes") {
return errors.New("dismissing a backlog requires --yes")
}
payload := map[string]string{}
if value := cmd.String("type"); value != "" {
payload["type"] = value
}
if value := cmd.String("before"); value != "" {
if _, err := time.Parse(time.RFC3339, value); err != nil {
return fmt.Errorf("invalid --before time %q", value)
}
payload["failed_before"] = value
}
client, opts, err := newAdminClientFromCommand(ctx, cmd)
if err != nil {
return err
}
var resp map[string]string
if err := client.postJSON(ctx, "/api/v1/tasks/"+url.PathEscape(taskID)+"/acknowledge", nil, &resp, false); err != nil {
var resp adminTaskAcknowledgeResult
if err := client.postJSON(ctx, "/api/v1/tasks/acknowledge", payload, &resp, false); err != nil {
return err
}
if opts.JSON {
return writeAdminJSON(cmd.Root().Writer, resp)
}
_, err = fmt.Fprintf(cmd.Root().Writer, "Task %s %s\n", taskID, resp["status"])
_, err = fmt.Fprintf(cmd.Root().Writer, "Dismissed %d failed tasks\n", resp.Acknowledged)
return err
},
},
Expand Down Expand Up @@ -1210,6 +1227,33 @@ func validateAdminRole(role string, allowEmpty bool) error {
}
}

type adminTaskAcknowledgeResult struct {
Acknowledged int `json:"acknowledged"`
}

func acknowledgeSingleTask(ctx context.Context, cmd *cli.Command) error {
taskID, err := requireSingleArg(cmd, "task id")
if err != nil {
return err
}
if _, err := strconv.ParseInt(taskID, 10, 64); err != nil {
return fmt.Errorf("invalid task id %q", taskID)
}
client, opts, err := newAdminClientFromCommand(ctx, cmd)
if err != nil {
return err
}
var resp map[string]string
if err := client.postJSON(ctx, "/api/v1/tasks/"+url.PathEscape(taskID)+"/acknowledge", nil, &resp, false); err != nil {
return err
}
if opts.JSON {
return writeAdminJSON(cmd.Root().Writer, resp)
}
_, err = fmt.Fprintf(cmd.Root().Writer, "Task %s %s\n", taskID, resp["status"])
return err
}

func requireSingleArg(cmd *cli.Command, label string) (string, error) {
if cmd.Args().Len() != 1 {
return "", fmt.Errorf("expected one %s argument", label)
Expand Down
12 changes: 11 additions & 1 deletion cmd/synaps3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"github.com/strahe/synaps3/internal/provider"
"github.com/strahe/synaps3/internal/synapse"
sdk "github.com/strahe/synapse-go"
sdkstorage "github.com/strahe/synapse-go/storage"
sdktypes "github.com/strahe/synapse-go/types"
"github.com/uptrace/bun"
"github.com/urfave/cli/v3"
)
Expand Down Expand Up @@ -316,7 +318,15 @@ func runServe(ctx context.Context, src config.Source) error {
}
defer func() { _ = client.Close() }()
resolvedAddresses := client.ResolvedAddresses()
storageClient := synapse.AdaptStorageService(client.Storage())
// What this node signs for. Destructive storage requests compare it against
// what a recorded request used, so a changed wallet or network stops them
// instead of acting on a data set ID that means something else there.
storageIdentity := sdkstorage.ContextIdentity{
Payer: client.Address(),
ChainID: sdktypes.ChainID(client.Chain().ChainID()),
RecordKeeper: resolvedAddresses.FWSS,
}
storageClient := synapse.AdaptStorageService(client.Storage(), client.WarmStorage(), storageIdentity)
walletQuerier := synapse.NewWalletQuerier(client.Payments(), client.Address(), client.Chain(), resolvedAddresses)
walletOperator := synapse.NewWalletOperator(client.Payments(), resolvedAddresses.USDFC)
filecoinReadiness := synapse.NewReadinessChecker(
Expand Down
2 changes: 1 addition & 1 deletion docs/en/concepts/filecoin-storage-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ One confirmation covers the whole move. SynapS3 creates the new storage service,

While replacements run, the Data Sets card shows every move that still needs progress or operator attention, including parallel moves on other replicas. Discovery uses an indeterminate progress bar because the final count is not known yet. Once discovery finishes, progress uses the processed share of the final total. Progress counts unique stored content, so content shared by several versions is copied once, and separates content transferred from content deleted before it needed to move.

Some steps wait rather than fail. The dashboard distinguishes creating the new service, waiting for it to become writable, an unreachable provider, wallet funds, and missing readable content. Most waits resume on their own. If an object has no other replica and no local cache, replacement waits until a source is available; this does not count toward the retry limit. Temporary copy failures resume after a restart and do not stop other content or another replacement. Use **Retry replacement** from the Data Sets list when the dashboard shows that content needs attention, or when shutting down the old provider needs a payment settled first. A target already in use cannot be retried; choose another provider.
Some steps wait rather than fail. The dashboard distinguishes creating the new service, waiting for it to become writable, an unreachable provider, wallet funds, and missing readable content. Most waits resume on their own. If an object has no other replica and no local cache, replacement waits until a source is available; this does not count toward the retry limit. Temporary copy failures resume after a restart and do not stop other content or another replacement. If a copy stops with a result that has to be checked, the replacement waits until you retry that copy's task on the Tasks page. If the result of shutting down the old provider is unclear, SynapS3 checks the chain and tries again on its own. Use **Retry replacement** from the Data Sets list when the dashboard shows that content needs attention, or when shutting down the old provider needs a payment settled first. A target already in use cannot be retried; choose another provider. A replica cannot be replaced again while an earlier replacement is still setting up its new storage service; wait for it to finish, or retry that setup on the Tasks page.

## What Users See

Expand Down
6 changes: 4 additions & 2 deletions docs/en/configuration/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ The Admin endpoint has separate exposure controls. Keep `admin.addr` on loopback

SQLite is the default and recommended database for SynapS3 single-node deployments. PostgreSQL remains available when a deployment already operates an external PostgreSQL service or needs an external metadata database. Keep its DSN in protected configuration or secret storage.

`database.max_open_conns` sizes the connection pool. SQLite still writes through one connection at a time; the other connections serve reads, and a write that finds the database busy waits up to five seconds (`busy_timeout`) before it fails with `SQLITE_BUSY`.

## Main Sections

| Section | Purpose |
Expand All @@ -86,7 +88,7 @@ SQLite is the default and recommended database for SynapS3 single-node deploymen
| `filecoin.network` | `calibration` |
| `filecoin.default_copies` | `3` |
| `database.driver` | `sqlite` |
| `database.max_open_conns` | `4` |
| `database.max_open_conns` | `32` |
| `database.max_idle_conns` | `2` |
| `cache.max_size_gb` | `100` |
| `cache.eviction_policy` | `lru` |
Expand All @@ -105,7 +107,7 @@ SQLite is the default and recommended database for SynapS3 single-node deploymen
| `admin.auth.username` | `admin` |
| `admin.auth.session_ttl` | `12h` |

`worker.tasks.concurrency` limits all background operations. Remote storage creation, Store, Pull, and commit submission additionally share `provider_mutation_concurrency`; remote cleanup and service retirement share `destructive_mutation_concurrency`. Status and confirmation checks do not consume either mutation limit. Wallet mutations are serialized. Task settings require a SynapS3 restart, and existing tasks retain the retry limit recorded when they were created.
`worker.tasks.concurrency` limits all background operations. Remote storage creation, Store, Pull, and commit submission additionally share `provider_mutation_concurrency`; remote cleanup and service retirement share `destructive_mutation_concurrency`. Status and confirmation checks do not consume either mutation limit. Wallet mutations are serialized. An operation that finds its limit full steps aside and tries again shortly instead of holding a `concurrency` slot, so other background work keeps running. Task settings require a SynapS3 restart, and existing tasks retain the retry limit recorded when they were created.

## Admin Session Lifetime

Expand Down
1 change: 1 addition & 0 deletions docs/en/getting-started/s3-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ The HTTP endpoints in these examples are for local evaluation. For production, u
| `AccessDenied` | Confirm the access key and secret key came from `synaps3 admin s3-user create`. |
| Client tries virtual-hosted buckets | Enable path-style addressing or equivalent client setting. |
| The first upload after creating a bucket returns `SlowDown` | The storage provider may still be preparing the bucket. Wait briefly, retry the upload, then confirm it with the read command. |
| Uploading data right after deleting its last version with `versionId` returns `SlowDown` | SynapS3 is still cleaning up the earlier copy of the same bytes. Wait a few minutes, then retry the upload. |
| Upload succeeds but Filecoin storage is pending | Check the dashboard task view or `synaps3 admin task list --status pending`. |
| Object size is rejected | Keep the object between `127` and `1,065,353,216` bytes. |
| Remote host cannot reach the admin dashboard | Keep admin on loopback and use `ssh -L 9090:127.0.0.1:9090 user@server`. |
2 changes: 1 addition & 1 deletion docs/en/operations/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Retry only after RPC connectivity, storage provider availability, wallet funds,
synaps3 admin task retry 42
```

The API decides whether each failed task can be retried safely. Provider replacement work is recovered from **Details** → **Storage** → **Data Sets**. A wallet operation can be recovered from Tasks only when no broadcast started; an uncertain broadcast remains non-retryable. An uncertain Store offers **Check again**, which observes the provider without uploading again. Use **Dismiss** or `synaps3 admin task acknowledge <id>` only after reviewing the failure; acknowledged tasks remain available for the configured retention period before cleanup.
The API decides whether each failed task can be retried safely. Provider replacement work is recovered from **Details** → **Storage** → **Data Sets**. A wallet operation can be recovered from Tasks only when no broadcast started; an uncertain broadcast remains non-retryable. An uncertain Store offers **Check again**, which observes the provider without uploading again. Use **Dismiss** or `synaps3 admin task acknowledge <id>` only after reviewing the failure; acknowledged tasks remain available for the configured retention period before cleanup. When failures have piled up, **Dismiss all** on the Tasks page clears the ones the current Operation filter selects, and `synaps3 admin task acknowledge --type <operation> --yes` does the same from the CLI; failures recorded after you confirm stay in the list.

## Provider or RPC Issues

Expand Down
22 changes: 22 additions & 0 deletions docs/en/reference/admin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,33 @@ If the attempt changed after it was inspected, the API returns `409 Conflict`. I
| `GET` | `/api/v1/tasks/stats` | Count tasks by status. |
| `POST` | `/api/v1/tasks/{id}/retry` | Recover a failed task when `retryable` is true. |
| `POST` | `/api/v1/tasks/{id}/acknowledge` | Dismiss a failed task when `acknowledgeable` is true. Acknowledgement starts its retention period, after which it may be cleaned up. |
| `GET` | `/api/v1/tasks/acknowledge/preview` | Count what a bulk dismissal would cover. Accepts the same optional `type`. Returns `count` and the `as_of` cutoff it counted at. |
| `POST` | `/api/v1/tasks/acknowledge` | Dismiss a backlog of failed tasks at once. Returns `acknowledged` with the number dismissed. |

`status` is `pending`, `running`, `completed`, `failed`, or `cancelled`. `presentation_status` renders pending work as `queued`, `scheduled`, or `waiting`, and acknowledged failures as `dismissed`. Responses also include `operation`, optional subject identity, and server-computed `retryable` and `acknowledgeable` flags.

The `status` filter also accepts `dismissed`. `status=failed` returns only unacknowledged failures, while `status=dismissed` returns acknowledged failures. `/api/v1/tasks/stats` reports those groups separately as `failed` and `dismissed`.

Bulk dismissal takes a JSON body with an optional `type` and an optional RFC 3339 `failed_before`, which defaults to the moment the request is handled:

```json
{
"type": "storage_store",
"failed_before": "2026-09-12T08:30:00Z"
}
```

Failures recorded after that moment stay visible, so a backlog can be cleared without hiding a failure nobody has reviewed. An unknown `type` or an unparsable `failed_before` returns `400 Bad Request`, as does a body with unknown fields, trailing content, or more than 4 KiB.

To show a number before dismissing, count first and then confirm with the cutoff that count was taken at:

```bash
curl -s "$ADMIN/api/v1/tasks/acknowledge/preview?type=storage_store"
# {"count":12,"as_of":"2026-09-12T08:30:00.123456789Z"}
```

Passing that `as_of` back as `failed_before` dismisses exactly what was counted.

`/api/v1/overview` groups `tasks.by_status` by `status`, so its `failed` count includes acknowledged failures. Use `tasks.attention.failed` for unacknowledged failures or `/api/v1/tasks/stats` for counts split between `failed` and `dismissed`.

Pagination is newest-first. When `next_cursor` is present, pass it as `cursor` to fetch the next page. Provider replacement recovery remains in the Data Sets API. Wallet operations are retryable only before a broadcast starts. Retrying an uncertain Store checks the provider and does not upload the bytes again.
Expand Down
3 changes: 2 additions & 1 deletion docs/en/reference/cli-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ synaps3 admin task stats
synaps3 admin task list --status failed --limit 100
synaps3 admin task retry 42
synaps3 admin task acknowledge 42
synaps3 admin task acknowledge --type storage_store --yes
synaps3 admin storage-confirmation list
synaps3 admin storage-confirmation release 42 --attempt-id current-attempt-id --yes
```
Expand All @@ -100,7 +101,7 @@ Admin global flags must appear after `admin` and before the subcommand:

Task listing supports `--type`, `--status`, `--limit`, and ID-based `--cursor`. Valid status filters are `pending`, `running`, `completed`, `failed`, `cancelled`, and `dismissed`. Pending work is presented as queued, scheduled, or waiting; `failed` returns unacknowledged failures and `dismissed` returns acknowledged failures.

`synaps3 admin task retry` recovers only tasks whose response says they are retryable. Provider replacement recovery remains under **Details** → **Storage** → **Data Sets**. A wallet operation can be retried only when its broadcast never started; an operation with an uncertain broadcast remains non-retryable. For an uncertain Store, the dashboard labels Retry as **Check again**: this checks the provider without uploading again. Use `synaps3 admin task acknowledge <id>` to dismiss a failed task after reviewing its outcome; acknowledgement starts its retention period, after which it may be cleaned up.
`synaps3 admin task retry` recovers only tasks whose response says they are retryable. Provider replacement recovery remains under **Details** → **Storage** → **Data Sets**. A wallet operation can be retried only when its broadcast never started; an operation with an uncertain broadcast remains non-retryable. For an uncertain Store, the dashboard labels Retry as **Check again**: this checks the provider without uploading again. Use `synaps3 admin task acknowledge <id>` to dismiss a failed task after reviewing its outcome; acknowledgement starts its retention period, after which it may be cleaned up. To clear a backlog, run it without an ID and confirm with `--yes`: `--type` limits it to one operation, and `--before` sets an RFC 3339 cutoff that defaults to now, so failures recorded later stay visible.

`synaps3 admin storage-confirmation list` shows storage confirmations that need review. Verify the piece CID, provider, current attempt ID, attempted time, and any available transaction evidence before running `storage-confirmation release <copy-id> --attempt-id <attempt-id> --yes`; release only if you accept that the provider may already store the piece and resubmission may create duplicate paid storage. A stale attempt ID is refused.

Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/s3-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ SynapS3 mainly supports path-style S3 access for writing bucket and object data

## Versioning Behavior

Buckets behave as versioning-enabled. A delete without `versionId` creates a delete marker. A delete with `versionId` either deletes that data version or delete marker, or returns an error; the gateway does not keep the request and run it later. Deleting a delete marker is not blocked by Filecoin storage. A data-version delete returns `400 InvalidRequest`, or an entry-specific `InvalidRequest` from `DeleteObjects`, while storage work is active or a submitted Filecoin transaction is awaiting confirmation. For an otherwise eligible data version, stopped storage work that has not submitted a transaction does not block deletion. After a data version is deleted, unreferenced remote storage is queued for background cleanup; storage shared with another version is preserved. Version listing returns object versions and delete markers.
Buckets behave as versioning-enabled. A delete without `versionId` creates a delete marker. A delete with `versionId` either deletes that data version or delete marker, or returns an error; the gateway does not keep the request and run it later. Deleting a delete marker is not blocked by Filecoin storage. A data-version delete returns `400 InvalidRequest`, or an entry-specific `InvalidRequest` from `DeleteObjects`, while storage work is active or a submitted Filecoin transaction is awaiting confirmation. For an otherwise eligible data version, stopped storage work that has not submitted a transaction does not block deletion. After a data version is deleted, unreferenced remote storage is queued for background cleanup; storage shared with another version is preserved. Cleanup asks the storage provider to remove the copy, which not every provider supports: a copy the provider will not or cannot remove may remain with it after the version is gone from the gateway. Until that cleanup finishes, usually within minutes, uploading the same bytes to the bucket returns `503 SlowDown`; a later retry stores them as new data. Version listing returns object versions and delete markers.

## What Is Intentionally Out of Scope

Expand Down
Loading