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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

#### [Unreleased](https://github.com/openfga/cli/compare/v0.7.0...HEAD)

Changed:
- Adjusted defaults for `--max-tuples-per-write`, `--max-parallel-requests`, `--max-rps`, and `--rampup-period-in-sec` when `--max-rps` is specified (#517).


#### [0.7.0](https://github.com/openfga/cli/compare/v0.6.6...v0.7.0) (2025-06-10)

> [!NOTE]
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,17 +671,18 @@ fga tuple **write** <user> <relation> <object> --store-id=<store-id>
* `--store-id`: Specifies the store id
* `--model-id`: Specifies the model id to target (optional)
* `--file`: Specifies the file name, `json`, `yaml` and `csv` files are supported
* `--max-tuples-per-write`: Max tuples to send in a single write (optional, default=1)
* `--max-parallel-requests`: Max requests to send in parallel (optional, default=4)
* `--max-tuples-per-write`: Max tuples to send in a single write (optional, default=1, or 40 if `--max-rps` is set and this flag is omitted)
* `--max-parallel-requests`: Max requests to send in parallel (optional, default=4, or `max-rps/5` if `--max-rps` is set and this flag is omitted)
* `--hide-imported-tuples`: When importing from a file, do not output successfully imported tuples in the command output (optional, default=false)
* `--max-rps`: Max requests per second, when set the CLI will ramp up requests from 1RPS to the set value over the set period. Used in conjunction with `--rampup-period-in-sec` (optional)
* `--rampup-period-in-sec`: Time in seconds to wait between each batch of tuples when ramping up. Used in conjunction with `--max-rps` (optional)
* `--max-rps`: Max requests per second. When set, the CLI will ramp up requests from 1 RPS to the set value. If `--rampup-period-in-sec` is omitted it defaults to `max-rps*2`.
* `--rampup-period-in-sec`: Time in seconds to wait between each batch of tuples when ramping up. Only used if `--max-rps` is set.
* All integer parameters must be greater than zero when provided.

###### Example (with arguments)
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap`
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap --condition-name inOffice --condition-context '{"office_ip":"10.0.1.10"}'`
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --model-id=01GXSA8YR785C4FYS3C0RTG7B1 --file tuples.json`
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv --max-tuples-per-write 10 --max-parallel-requests 5 --max-rps 10 --rampup-period-in-sec 10`
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv --max-rps 10`

###### Response
```json5
Expand Down
80 changes: 75 additions & 5 deletions cmd/tuple/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ var writeCmd = &cobra.Command{
fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.yaml
fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv
fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv --max-tuples-per-write 10 --max-parallel-requests 5
fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv --max-tuples-per-write 10 --max-parallel-requests 5 --max-rps 10 --rampup-period-in-sec 10`,
fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv --max-rps 10`,
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils.GetClientConfig(cmd)

Expand All @@ -97,6 +97,30 @@ func writeTuplesFromArgs(cmd *cobra.Command, args []string, fgaClient *client.Op
return err //nolint:wrapcheck
}

maxTuplesPerWrite, err := cmd.Flags().GetInt("max-tuples-per-write")
if err != nil {
return fmt.Errorf("failed to parse max-tuples-per-write due to %w", err)
}

maxParallelRequests, err := cmd.Flags().GetInt("max-parallel-requests")
if err != nil {
return fmt.Errorf("failed to parse max-parallel-requests due to %w", err)
}

maxRPS, err := cmd.Flags().GetInt("max-rps")
if err != nil {
return fmt.Errorf("failed to parse max-rps due to %w", err)
}

rampUpPeriodInSec, err := cmd.Flags().GetInt("rampup-period-in-sec")
if err != nil {
return fmt.Errorf("failed to parse rampup-period-in-sec due to %w", err)
}

if err := validateWriteFlags(cmd.Flags(), maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec); err != nil {
return err
}

body := client.ClientWriteTuplesBody{
client.ClientTupleKey{
User: args[0],
Expand All @@ -122,6 +146,48 @@ func writeTuplesFromArgs(cmd *cobra.Command, args []string, fgaClient *client.Op
)
}

func validateWriteFlags(flags *flag.FlagSet, maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec int) error {
if flags.Changed("max-tuples-per-write") && maxTuplesPerWrite <= 0 {
return errors.New("max-tuples-per-write must be greater than zero") //nolint:err113
}

if flags.Changed("max-parallel-requests") && maxParallelRequests <= 0 {
return errors.New("max-parallel-requests must be greater than zero") //nolint:err113
}

if flags.Changed("max-rps") && maxRPS <= 0 {
return errors.New("max-rps must be greater than zero") //nolint:err113
}

if flags.Changed("rampup-period-in-sec") && rampUpPeriodInSec <= 0 {
return errors.New("rampup-period-in-sec must be greater than zero") //nolint:err113
}

return nil
}

func applyWriteDefaults(flags *flag.FlagSet, maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec int) (int, int, int, int) {
if maxRPS > 0 && !flags.Changed("rampup-period-in-sec") {
rampUpPeriodInSec = maxRPS * tuple.RPSToRampupPeriodMultiplier
}

if maxRPS > 0 && !flags.Changed("max-parallel-requests") {
defaultParallel := maxRPS / tuple.RPSToParallelRequestsDivisor

if defaultParallel < 1 {
defaultParallel = 1
}

maxParallelRequests = defaultParallel
}

if maxRPS > 0 && !flags.Changed("max-tuples-per-write") {
maxTuplesPerWrite = tuple.DefaultMaxTuplesPerWriteWithRPS
}

return maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec
}

func writeTuplesFromFile(ctx context.Context, flags *flag.FlagSet, fgaClient *client.OpenFgaClient) error { //nolint:cyclop
startTime := time.Now()

Expand Down Expand Up @@ -154,6 +220,14 @@ func writeTuplesFromFile(ctx context.Context, flags *flag.FlagSet, fgaClient *cl
return fmt.Errorf("failed to parse parallel requests due to %w", err)
}

if err := validateWriteFlags(flags, maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec); err != nil {
return err
}

maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec = applyWriteDefaults(
flags, maxTuplesPerWrite, maxParallelRequests, maxRPS, rampUpPeriodInSec,
)

debug, err := flags.GetBool("debug")
if err != nil {
return fmt.Errorf("failed to parse debug flag due to %w", err)
Expand Down Expand Up @@ -209,10 +283,6 @@ func init() {

writeCmd.Flags().Int("max-rps", 0, "The maximum requests per second.")
writeCmd.Flags().Int("rampup-period-in-sec", 0, "The period over which to ramp up the request rate.")
writeCmd.MarkFlagsRequiredTogether(
"max-rps",
"rampup-period-in-sec",
)

writeCmd.Flags().BoolVar(&hideImportedTuples, "hide-imported-tuples", false, "Hide successfully imported tuples from output")
}
9 changes: 9 additions & 0 deletions internal/tuple/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ const (

// DefaultMinRPS Default minimum requests per second.
DefaultMinRPS = 1

// DefaultMaxTuplesPerWriteWithRPS is the tuples per write when --max-rps is set but --max-tuples-per-write is omitted.
DefaultMaxTuplesPerWriteWithRPS = 40

// RPSToParallelRequestsDivisor defines how max-rps translates to max parallel requests.
RPSToParallelRequestsDivisor = 5

// RPSToRampupPeriodMultiplier defines how max-rps translates to ramp-up period.
RPSToRampupPeriodMultiplier = 2
)

type failedWriteResponse struct {
Expand Down
Loading