Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
3392f09
pull initial work from branch load-testing
cjonas9 May 9, 2026
ffe27bc
add ledger generation test adapted for RPC
cjonas9 May 4, 2026
65da3b5
add apply load config
cjonas9 May 4, 2026
34f086d
add generated ledger output to infrastructure/testdata/
cjonas9 May 4, 2026
80c982d
add basic ingestion of synthetic ledgers phase
cjonas9 May 7, 2026
94c69b7
disable debug logs for load test for timeout reasons
cjonas9 May 7, 2026
f4a16f9
add functions for snapshotting + restoring test DB
cjonas9 May 8, 2026
1d53b96
improve ad restructure db restoration helpers/API
cjonas9 May 8, 2026
baf2255
finish DB restoration logic flow and wiring
cjonas9 May 8, 2026
1647464
skip migrations/fee-stats in load test mode
cjonas9 May 8, 2026
2f14765
ingest test: refactor, minor semantic fixes
cjonas9 May 8, 2026
d7c90a9
test.go: add retention window to config, fix fake history archive for…
cjonas9 May 8, 2026
0390757
minor db restore/trim helper fixes
cjonas9 May 8, 2026
e0a86e7
rename restore backed-up ledgers function for accuracy
cjonas9 May 8, 2026
f151a35
refactor, add env vars, change DB helpers to take sequences
cjonas9 May 12, 2026
bffb101
remove db restoration functionality
cjonas9 May 15, 2026
e04d51d
add performance metrics json emission functionality
cjonas9 May 15, 2026
bd8c784
migrate to polling getHealth, change ingest test limits to 1000 ledgers
cjonas9 May 15, 2026
c7bc001
remove ledger fixtures
cjonas9 May 15, 2026
786423d
add workflow and script
cjonas9 May 15, 2026
1606829
fix yaml referencing wrong path for script
cjonas9 May 15, 2026
7d41b1a
fix yml parsing indentation bug
cjonas9 May 15, 2026
b701108
use head-object for metadata rather than tags
cjonas9 May 15, 2026
b1cec1d
refine workflow + instance script
cjonas9 May 15, 2026
b9ef27e
add apply load cfg
cjonas9 May 15, 2026
73df1e7
testing: on-push runs
cjonas9 May 15, 2026
241bdf8
minor yml syntax fixes
cjonas9 May 15, 2026
1161e3f
set test e2e.yml + add debugging info from instance to ssm
cjonas9 May 15, 2026
47437f4
skip e2e.yml for testing, add retry loop for root volume lookup
cjonas9 May 15, 2026
008f327
build-libs over build-stellar-rpc to cut time back
cjonas9 May 15, 2026
9441749
further slim build phase with no-install-recommends
cjonas9 May 15, 2026
36c0a82
make instance script best-effort if head-object or stellar-core versi…
cjonas9 May 16, 2026
4535977
temporarily modify script to work on scratch dev box; increase timeout
cjonas9 May 16, 2026
54f0b41
fix cfg path error and run ID regression
cjonas9 May 16, 2026
44e093c
improve error logging
cjonas9 May 16, 2026
1ce515e
fix error logging wrapper
cjonas9 May 16, 2026
22a8e6e
patch premature exit due to err trap bug
cjonas9 May 16, 2026
e925d3c
fix empty GOPATH/GOMODCACHE
cjonas9 May 16, 2026
ffb3299
updated apply-load config for specific core on runner
cjonas9 May 16, 2026
bae2fdc
fix version check if warning prints
cjonas9 May 16, 2026
70eb9dc
bump all timeouts to >= 2 hours
cjonas9 May 17, 2026
705edec
increase ingest phase timeout
cjonas9 May 17, 2026
c697510
extend aws role lifetime
cjonas9 May 17, 2026
caec3f4
undo session time limit increase, use pre-generated synthetic ledgers
cjonas9 May 18, 2026
b32181d
require confirmed gp3 throttling, extend GHA AWS session to 4 hours
cjonas9 May 19, 2026
3a0571f
patch logic for throttling behavior
cjonas9 May 19, 2026
2c4a004
slim needless instance bootstrapping work
cjonas9 May 20, 2026
d620450
refactor ephemeral load test runner
cjonas9 May 27, 2026
41529f4
fix minor refactor false sha-verify failure
cjonas9 May 27, 2026
2a21c73
add support for multiple ledger profiles/files
cjonas9 Jun 11, 2026
04f0fe1
change log level to warn, decrease each soroban scenario meta to 1000…
cjonas9 Jun 12, 2026
02556cc
refactored duplicate/messy code
cjonas9 Jun 12, 2026
807ca42
stop grepping to determine success status, make GHA->ec2 parameter pa…
cjonas9 Jun 12, 2026
af9f7b9
split offline ledger generation out of the ingest benchmark
cjonas9 Jun 12, 2026
e4180dd
refactor and remove old apply load cfg
cjonas9 Jun 12, 2026
4c3394c
Merge remote-tracking branch 'origin/main' into apply-load
cjonas9 Jun 12, 2026
bc2e490
Merge branch 'main' into apply-load
cjonas9 Jun 15, 2026
555483e
Merge branch 'apply-load' of https://github.com/stellar/stellar-rpc i…
cjonas9 Jun 15, 2026
a620cdc
fix linter errors
cjonas9 Jun 15, 2026
65cb6ff
update go version
cjonas9 Jun 17, 2026
4aca1ef
update default ledger bundle/config to existent ones for test
cjonas9 Jun 17, 2026
3d61ceb
decompose ec2 script into go programs
cjonas9 Jun 17, 2026
9bef115
reduce comment verbosity, minor clean up
cjonas9 Jun 17, 2026
8b39ed5
install jq on load-test box for build-libs; surface build-libs errors
cjonas9 Jun 18, 2026
714b1fa
throttle load-test benchmark via cgroup io.max instead of EBS ModifyV…
cjonas9 Jun 18, 2026
d9987f6
drop accidentally-committed refresh tooling and orphaned apply-load.cfg
cjonas9 Jun 18, 2026
5058ad3
fix linter errors in load-test runner
cjonas9 Jun 18, 2026
8cf9f6b
make load-test ingest frequency and ledger count configurable via env
cjonas9 Jun 18, 2026
3b33626
EXPERIMENT: run load-test benchmark un-throttled at volume-provisione…
cjonas9 Jun 18, 2026
7e60661
use SDK's maxLedgersPerFile ceiling and multiple-bundle functionality
cjonas9 Jun 18, 2026
fdb1926
simplify verification walk
cjonas9 Jun 18, 2026
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 .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ target/
storage/
.soroban/
.cargo/
.cargo-husky/
.cargo-husky/
6 changes: 5 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ concurrency:

jobs:
run-system-test:
# Temporary branch used to iterate on the EC2-backed load-test workflow.
# The reusable system-test workflow is currently rejected for this PR path,
# so skip e2e here instead of failing every push to apply-load.
if: github.event_name != 'pull_request' || github.head_ref != 'apply-load'
# set the git ref of stellar/system-test after the '@'
# to specify which version of the workflow to call
uses: stellar/system-test/.github/workflows/test-workflow.yml@master
uses: stellar/system-test/.github/workflows/test-workflow.yml@789121c0914150a02122581f32ee62c4e42e1c84

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.

what's the motivation for this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm glad you caught this and I'm going to leave this thread open so I remember to fix it, but if I remember correctly, this had to do with some weirdness in trying to get the GHA to trigger without anything being merged to main. I'm near-certain this won't end up being here after all PR review changes are made and this is verified as working

with:
stellar-rpc-repo: "${{ github.repository }}"
stellar-rpc-ref: "${{ github.ref }}"
Expand Down
213 changes: 213 additions & 0 deletions .github/workflows/load-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
name: Load test (ephemeral)
# Launches a c5.2xlarge in Horizon (203618453975), polls it via SSM, posts

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.

what is Horizon (203618453975) ? what is SSM ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are some AWS details, the former is the account ID for profile Horizon, the latter is AWS Systems Manager. I figured that info could be helpful to someone maintaining this (and for their agent that's scanning this and needs to know that)

# results to the PR, terminates. Box bootstrap lives in run-load-test.sh;
# runner-side polling in runner/orchestrate.go.

on:
push:
branches: [apply-load]
Comment thread
cjonas9 marked this conversation as resolved.

permissions:
id-token: write # for OIDC AssumeRole into the GHA role
contents: read
pull-requests: write

jobs:
load-test:
name: Launch + await ephemeral load-test box
runs-on: ubuntu-latest
timeout-minutes: 225 # 210min results wait + buffer for boot/SSM/poll latency and cleanup (role lasts 240min)
env:
AWS_REGION: us-east-1
INSTANCE_TYPE: c5.2xlarge
ROOT_VOLUME_GB: 500
BOOTSTRAP_VOLUME_IOPS: 3000
# 3000 IOPS is the gp3 floor; 125 MiB/s alone would need only 500.
BOOTSTRAP_VOLUME_THROUGHPUT: 125
INSTANCE_PROFILE: stellar-rpc-ci-load-test
TEST_TAG_KEY: test
TEST_TAG_VAL: stellar-rpc-ci-load-test
SSM_REGISTRATION_TIMEOUT: 240 # SSM agent registers ~30-90s after boot
RESULTS_TIMEOUT: 12600 # 210 min wait for /tmp/done: ~55m bootstrap+build + ~90m benchmark, under the 170m go-test budget.
POLL_INTERVAL: 30
DEBUG_LOG_LINES: 40
DEBUG_LOG_EVERY_POLLS: 5
LOAD_TEST_DIR: cmd/stellar-rpc/internal/integrationtest/infrastructure/load-test

steps:
- name: Resolve target context
id: target
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER=$(gh pr list \
--repo "${{ github.repository }}" \
--state open \
--base main \
--head "${{ github.ref_name }}" \
--json number \
--jq '.[0].number // ""' 2>/dev/null || true)

RUN_LABEL="${PR_NUMBER:+pr$PR_NUMBER}"
{
echo "pr_number=$PR_NUMBER"
echo "pr_tag_value=${PR_NUMBER:-none}"
echo "run_label=${RUN_LABEL:-${{ github.ref_name }}}"
} >> "$GITHUB_OUTPUT"

- name: Checkout target ref
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}

# The runner-side half is `go run ... runner orchestrate`.
- uses: ./.github/actions/setup-go

- name: Configure AWS via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_GHA_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
role-duration-seconds: 14400

- name: Resolve latest Ubuntu 22.04 AMI
id: ami
run: |
AMI=$(aws ec2 describe-images \
--owners 099720109477 \
--filters \
"Name=name,Values=ubuntu/images/hvm-ssd*/ubuntu-jammy-22.04-amd64-server-*" \
"Name=architecture,Values=x86_64" \
"Name=state,Values=available" \
--query 'sort_by(Images, &CreationDate)[-1].ImageId' \
--output text)
echo "ami=$AMI" >> "$GITHUB_OUTPUT"

- name: Render user-data
# The script ships verbatim; parameters travel in a two-line preamble
# so the bytes that run on the box match the bytes in git.
run: |
{
echo '#!/usr/bin/env bash'
echo 'export TARGET_SHA=${{ github.sha }} RUN_ID=${{ github.run_id }}'
cat "$LOAD_TEST_DIR/run-load-test.sh"
} > /tmp/user-data.sh

- name: Launch EC2 instance
id: launch
run: |
COMMON_TAGS="{Key=$TEST_TAG_KEY,Value=$TEST_TAG_VAL},
{Key=pr,Value=${{ steps.target.outputs.pr_tag_value }}},
{Key=ref,Value=${{ github.ref_name }}},
{Key=sha,Value=${{ github.sha }}},
{Key=run-id,Value=${{ github.run_id }}}"
RUN_INSTANCES_JSON=$(aws ec2 run-instances \
--image-id "${{ steps.ami.outputs.ami }}" \
--instance-type "$INSTANCE_TYPE" \
--iam-instance-profile "Name=$INSTANCE_PROFILE" \
--user-data file:///tmp/user-data.sh \
--block-device-mappings "[{
\"DeviceName\":\"/dev/sda1\",
\"Ebs\":{\"VolumeSize\":$ROOT_VOLUME_GB,\"VolumeType\":\"gp3\",\"Iops\":$BOOTSTRAP_VOLUME_IOPS,\"Throughput\":$BOOTSTRAP_VOLUME_THROUGHPUT,\"DeleteOnTermination\":true}
}]" \
--tag-specifications \
"ResourceType=instance,Tags=[
{Key=Name,Value=load-test-${{ steps.target.outputs.run_label }}},
$COMMON_TAGS
]" \
"ResourceType=volume,Tags=[
{Key=Name,Value=load-test-${{ steps.target.outputs.run_label }}-root},
$COMMON_TAGS
]" \
--count 1 \
--output json)

INSTANCE_ID=$(printf '%s' "$RUN_INSTANCES_JSON" | jq -r '.Instances[0].InstanceId')
echo "instance_id=$INSTANCE_ID" >> "$GITHUB_OUTPUT"

- name: Acknowledge launch in PR
if: steps.target.outputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
if ! gh pr comment ${{ steps.target.outputs.pr_number }} \
--repo ${{ github.repository }} \
--body "⏳ Load test launching on \`${{ steps.launch.outputs.instance_id }}\` (commit \`${{ github.sha }}\`).
Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Posting results when the run finishes."; then
echo "::warning::Failed to post launch comment to PR #${{ steps.target.outputs.pr_number }}"
fi

- name: Wait for SSM agent to register
env:
INSTANCE_ID: ${{ steps.launch.outputs.instance_id }}
run: |
DEADLINE=$(( $(date +%s) + SSM_REGISTRATION_TIMEOUT ))
while [ $(date +%s) -lt $DEADLINE ]; do
PING=$(aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
--query 'InstanceInformationList[0].PingStatus' \
--output text 2>/dev/null || echo "")
echo "[$(date -u +%FT%TZ)] ssm ping=$PING"
if [ "$PING" = "Online" ]; then
exit 0
fi
sleep 10
done
echo "::error::SSM agent never registered for $INSTANCE_ID — verify AmazonSSMManagedInstanceCore is attached to the stellar-rpc-ci-load-test role"
exit 1

- name: Poll for results
id: results
env:
INSTANCE_ID: ${{ steps.launch.outputs.instance_id }}
run: go run "./$LOAD_TEST_DIR/runner" orchestrate

- name: Write results summary
if: always()
run: |
if [ -f /tmp/results.md ]; then
cat /tmp/results.md >> "$GITHUB_STEP_SUMMARY"
elif [ -f /tmp/timeout-comment.md ]; then
cat /tmp/timeout-comment.md >> "$GITHUB_STEP_SUMMARY"
fi

- name: Post results to PR
if: steps.target.outputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ steps.results.outputs.found }}" = "true" ]; then
BODY=/tmp/results.md
else
BODY=/tmp/timeout-comment.md
fi
if [ ! -s "$BODY" ]; then
echo "::warning::No body to post to PR #${{ steps.target.outputs.pr_number }} ($BODY missing or empty)"
exit 0
fi
if ! gh pr comment ${{ steps.target.outputs.pr_number }} \
--repo ${{ github.repository }} \
--body-file "$BODY"; then
echo "::warning::Failed to post comment to PR #${{ steps.target.outputs.pr_number }}"
fi

- name: Fail workflow on timeout or load-test failure
if: always()
run: |
if [ "${{ steps.results.outputs.found }}" != "true" ]; then
echo "Load test timed out before producing instance results"
exit 1
fi

if [ "${{ steps.results.outputs.passed }}" != "true" ]; then
echo "Instance reported a failing verdict"
cat /tmp/results.md 2>/dev/null || true
exit 1
fi

- name: Terminate instance
if: always() && steps.launch.outputs.instance_id != ''
run: |
aws ec2 terminate-instances \
--instance-ids ${{ steps.launch.outputs.instance_id }} || true
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ captive-core/
.soroban/
!test.toml
*.sqlite*

# Generated load-test ledger corpora (hundreds of MB; canonical copies live in
# s3://stellar-rpc-ci-load-test/ledgers/).
cmd/stellar-rpc/internal/integrationtest/infrastructure/testdata/*.xdr.zstd
cmd/stellar-rpc/internal/integrationtest/infrastructure/load-test/testdata/*.xdr.zstd

# Compiled refresh tool (build artifact; rebuild with `go build` in refresh/).
cmd/stellar-rpc/internal/integrationtest/infrastructure/load-test/refresh/refresh-tool
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Integration tests:

```bash
STELLAR_RPC_INTEGRATION_TESTS_ENABLED=true \
STELLAR_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=23 \
STELLAR_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=25 \
STELLAR_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=$(which stellar-core) \
go test -v -failfast ./cmd/stellar-rpc/internal/integrationtest/...
```
Expand Down
61 changes: 45 additions & 16 deletions cmd/stellar-rpc/internal/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,28 +68,57 @@ type Config struct {
RequestBacklogSimulateTransactionQueueLimit uint
RequestBacklogGetFeeStatsTransactionQueueLimit uint
RequestExecutionWarningThreshold time.Duration
MaxRequestExecutionDuration time.Duration
MaxGetHealthExecutionDuration time.Duration
MaxGetEventsExecutionDuration time.Duration
MaxGetNetworkExecutionDuration time.Duration
MaxGetVersionInfoExecutionDuration time.Duration
MaxGetLatestLedgerExecutionDuration time.Duration
MaxGetLedgerEntriesExecutionDuration time.Duration
MaxGetTransactionExecutionDuration time.Duration
MaxGetTransactionsExecutionDuration time.Duration
MaxGetLedgersExecutionDuration time.Duration
MaxSendTransactionExecutionDuration time.Duration
MaxSimulateTransactionExecutionDuration time.Duration
MaxGetFeeStatsExecutionDuration time.Duration
ServeLedgersFromDatastore bool
BufferedStorageBackendConfig ledgerbackend.BufferedStorageBackendConfig
DataStoreConfig datastore.DataStoreConfig

MaxRequestExecutionDuration time.Duration
MaxGetHealthExecutionDuration time.Duration
MaxGetEventsExecutionDuration time.Duration
MaxGetNetworkExecutionDuration time.Duration
MaxGetVersionInfoExecutionDuration time.Duration
MaxGetLatestLedgerExecutionDuration time.Duration
MaxGetLedgerEntriesExecutionDuration time.Duration
MaxGetTransactionExecutionDuration time.Duration
MaxGetTransactionsExecutionDuration time.Duration
MaxGetLedgersExecutionDuration time.Duration
MaxSendTransactionExecutionDuration time.Duration
MaxSimulateTransactionExecutionDuration time.Duration
MaxGetFeeStatsExecutionDuration time.Duration

ServeLedgersFromDatastore bool
BufferedStorageBackendConfig ledgerbackend.BufferedStorageBackendConfig
DataStoreConfig datastore.DataStoreConfig

LoadTest LoadTestConfig

// We memoize these, so they bind to pflags correctly
optionsCache *Options
flagset *pflag.FlagSet
}

// LoadTestConfig groups the options for ingesting from pre-generated synthetic
// ledger bundles. If no files are given, normal captive-core ingestion runs.
type LoadTestConfig struct {
// Files are .xdr.zstd bundles of LedgerCloseMeta records produced by
// stellar-core's apply-load, replayed in order.
Files []string `toml:"files"`
// Frequency paces ingestion, replaying one synthetic ledger per duration.
// Zero means "use DefaultLoadTestFrequency".
Frequency time.Duration `toml:"frequency"`
// MaxLedgersPerFile optionally caps how many ledgers are replayed from each
// file in Files. Zero replays every ledger in every file.
MaxLedgersPerFile uint32 `toml:"max_ledgers_per_file"`
}

// Enabled reports whether the daemon should ingest from synthetic ledger
// bundles instead of captive core.
func (cfg LoadTestConfig) Enabled() bool {
return len(cfg.Files) > 0
}

// DefaultLoadTestFrequency is the pacing used when LoadTestConfig.Frequency
// is unset. Applied at the daemon's use-site rather than at config-load time
// so it survives the TOML-only configuration path.
const DefaultLoadTestFrequency = 2 * time.Second

func (cfg *Config) ExtendedUserAgent(extension string) string {
if cfg.HistoryArchiveUserAgent == "" {
return extension
Expand Down
41 changes: 31 additions & 10 deletions cmd/stellar-rpc/internal/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,11 +617,7 @@ func (cfg *Config) options() Options {
return unmarshalTOMLTree(i, option.ConfigKey, "buffered_storage_backend_config")
},
MarshalTOML: func(_ *Option) (any, error) {
tomlBytes, err := toml.Marshal(defaultBufferedStorageBackendConfig())
if err != nil {
return nil, fmt.Errorf("failed to marshal buffered_storage_backend_config: %w", err)
}
return toml.LoadBytes(tomlBytes)
return marshalTOMLTree(defaultBufferedStorageBackendConfig(), "buffered_storage_backend_config")
},
},
{
Expand All @@ -632,11 +628,20 @@ func (cfg *Config) options() Options {
return unmarshalTOMLTree(i, option.ConfigKey, "datastore_config")
},
MarshalTOML: func(_ *Option) (any, error) {
tomlBytes, err := toml.Marshal(defaultDataStoreConfig())
if err != nil {
return nil, fmt.Errorf("failed to marshal datastore_config: %w", err)
}
return toml.LoadBytes(tomlBytes)
return marshalTOMLTree(defaultDataStoreConfig(), "datastore_config")
},
},
{
TomlKey: "load_test_config",
ConfigKey: &cfg.LoadTest,
Usage: "Load testing configuration: replay pre-generated .xdr.zstd ledger bundles " +
"through ingestion. Subkeys: files (list of bundle paths), frequency (duration; " +
"defaults to 2s), max_ledgers_per_file (0 = all). WARNING: destructive to your database.",
CustomSetValue: func(option *Option, i any) error {
return unmarshalTOMLTree(i, option.ConfigKey, "load_test_config")
},
MarshalTOML: func(_ *Option) (any, error) {
return marshalTOMLTree(defaultLoadTestConfig(), "load_test_config")
},
},
}
Expand Down Expand Up @@ -664,6 +669,22 @@ func defaultDataStoreConfig() datastore.DataStoreConfig {
}
}

func defaultLoadTestConfig() LoadTestConfig {
return LoadTestConfig{
Frequency: DefaultLoadTestFrequency,
}
}

// marshalTOMLTree renders a sub-config struct as the TOML tree the option's
// MarshalTOML hook must return.
func marshalTOMLTree(v any, configName string) (any, error) {
tomlBytes, err := toml.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal %s: %w", configName, err)
}
return toml.LoadBytes(tomlBytes)
}

func unmarshalTOMLTree(tree any, out any, configName string) error {
t, ok := tree.(*toml.Tree)
if !ok {
Expand Down
Loading
Loading