temporalio/temporal · error

ErrNonPositiveTotalNumShards

ErrNonPositiveTotalNumShards

Error message

%w: %d

What it means

NewOwnershipBasedQuotaScaler validates that totalNumShards is positive before constructing the scaler. The returned error wraps ErrNonPositiveTotalNumShards with the offending value, guarding against division/mapping errors in rate-limit scaling math driven by the total shard count.

Source

Thrown at service/history/shard/ownership_based_quota_scaler.go:66

var (
	// shardCountNotSet is a sentinel value for the shardCount which indicates that it hasn't been set yet. It's an
	// int64 because that's the type of the atomic.
	shardCountNotSet int64 = -1

	ErrNonPositiveTotalNumShards = errors.New("totalNumShards must be greater than 0")
)

// NewOwnershipBasedQuotaScaler returns an OwnershipBasedQuotaScaler. The updateAppliedCallback field is a channel which
// is sent to in a blocking fashion when the shard count updates are applied. This is useful for testing. In production,
// you should pass in nil, which will cause the callback to be ignored. If totalNumShards is non-positive, then an error
// is returned.
func NewOwnershipBasedQuotaScaler(
	shardCounter ShardCounter,
	totalNumShards int,
	updateAppliedCallback chan struct{},
) (*OwnershipBasedQuotaScalerImpl, error) {
	if totalNumShards <= 0 {
		return nil, fmt.Errorf("%w: %d", ErrNonPositiveTotalNumShards, totalNumShards)
	}

	scaler := &OwnershipBasedQuotaScalerImpl{
		shardCounter:          shardCounter,
		totalNumShards:        totalNumShards,
		updateAppliedCallback: updateAppliedCallback,
		subscription:          shardCounter.SubscribeShardCount(),
	}

	scaler.shardCount.Store(shardCountNotSet)
	scaler.shutdownWG.Go(func() {

		for count := range scaler.subscription.ShardCount() {
			scaler.shardCount.Store(int64(count))
			if scaler.updateAppliedCallback != nil {
				scaler.updateAppliedCallback <- struct{}{}
			}
		}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Pass the cluster's actual numHistoryShards (positive) from config when constructing the scaler
  2. Fix the config/dynamicconfig entry that supplies 0 or a negative shard count
  3. Ensure the ShardCounter is initialized before scaler construction so the total is populated

Example fix

// before
scaler, err := NewOwnershipBasedQuotaScaler(counter, cfg.NumShards, cb)
// after
if cfg.NumShards <= 0 {
	return fmt.Errorf("invalid numHistoryShards: %d", cfg.NumShards)
}
scaler, err := NewOwnershipBasedQuotaScaler(counter, cfg.NumShards, cb)
Defensive patterns

Strategy: validation

Validate before calling

if totalNumShards <= 0 {
	return fmt.Errorf("totalNumShards must be positive, got %d", totalNumShards)
}

Try / catch

scaler, err := NewOwnershipBasedQuotaScaler(counter, n, cb)
if errors.Is(err, ErrNonPositiveTotalNumShards) {
	// fix configuration before proceeding; do not start with invalid scaling
}

Prevention

When it happens

Trigger: Calling NewOwnershipBasedQuotaScaler with totalNumShards <= 0 — e.g. from an uninitialized shard count source, a config value of 0, or a shardCounter whose total was not yet populated.

Common situations: Dynamic config where numHistoryShards is unset/0; a service started before the shard counter was initialized; tests constructing the scaler with a zero default; misconfigured cluster sizing.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/216428d6fcae4534. Report an issue: GitHub.