Skip to content

Commit 29bdad3

Browse files
committed
Implement configurable state chunk size
1 parent d7a4964 commit 29bdad3

File tree

5 files changed

+670
-461
lines changed

5 files changed

+670
-461
lines changed

docs/plugin-protocol/tfplugin6.proto

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,12 +915,22 @@ message ConfigureStateStore {
915915
message Request {
916916
string type_name = 1;
917917
DynamicValue config = 2;
918+
StateStoreClientCapabilities capabilities = 3;
918919
}
919920
message Response {
920921
repeated Diagnostic diagnostics = 1;
922+
StateStoreServerCapabilities capabilities = 2;
921923
}
922924
}
923925

926+
message StateStoreClientCapabilities {
927+
int64 chunk_size = 1; // suggested chunk size by Core
928+
}
929+
930+
message StateStoreServerCapabilities {
931+
int64 chunk_size = 1; // chosen chunk size by plugin
932+
}
933+
924934
message ReadStateBytes {
925935
message Request {
926936
string type_name = 1;

internal/command/meta_backend.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ import (
4141
"github.com/hashicorp/terraform/internal/tfdiags"
4242
)
4343

44+
const (
45+
// defaultStateStoreChunkSize is the default chunk size proposed
46+
// to the provider.
47+
// This can be tweaked but should provide reasonable performance
48+
// trade-offs for average network conditions and state file sizes.
49+
defaultStateStoreChunkSize int64 = 8 << 20 // 8 MB
50+
51+
// maxStateStoreChunkSize is the highest chunk size provider may choose
52+
// which we still consider reasonable/safe.
53+
// This reflects terraform-plugin-go's max. RPC message size of 256MB
54+
// and leaves plenty of space for other variable data like diagnostics.
55+
maxStateStoreChunkSize int64 = 128 << 20 // 128 MB
56+
)
57+
4458
// BackendOpts are the options used to initialize a backendrun.OperationsBackend.
4559
type BackendOpts struct {
4660
// BackendConfig is a representation of the backend configuration block given in

internal/plugin6/grpc_provider.go

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ func (p *GRPCProviderPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Serve
4848
return nil
4949
}
5050

51+
// grpcMaxMessageSize is the maximum gRPC send and receive message sizes
52+
// This matches the maximum set by a server implemented via terraform-plugin-go.
53+
// See https://github.com/hashicorp/terraform-plugin-go/blob/a361c9bf/tfprotov6/tf6server/server.go#L88
54+
const grpcMaxMessageSize = 256 << 20
55+
5156
// GRPCProvider handles the client, or core side of the plugin rpc connection.
5257
// The GRPCProvider methods are mostly a translation layer between the
5358
// terraform providers types and the grpc proto types, directly converting
@@ -75,8 +80,12 @@ type GRPCProvider struct {
7580

7681
// schema stores the schema for this provider. This is used to properly
7782
// serialize the requests for schemas.
78-
mu sync.Mutex
7983
schema providers.GetProviderSchemaResponse
84+
// stateChunkSize stores the negotiated chunk size for any implemented
85+
// state store (keyed by type name) that have gone through successful configuration.
86+
stateChunkSize map[string]int
87+
88+
mu sync.Mutex
8089
}
8190

8291
func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
@@ -1503,25 +1512,38 @@ func (p *GRPCProvider) ConfigureStateStore(r providers.ConfigureStateStoreReques
15031512
return resp
15041513
}
15051514

1515+
clientCapabilities := stateStoreClientCapabilitiesToProto(r.Capabilities)
1516+
logger.Trace("GRPCProvider.v6: ConfigureStateStore: proposing client capabilities", clientCapabilities)
1517+
15061518
protoReq := &proto6.ConfigureStateStore_Request{
15071519
TypeName: r.TypeName,
15081520
Config: &proto6.DynamicValue{
15091521
Msgpack: mp,
15101522
},
1523+
Capabilities: clientCapabilities,
15111524
}
15121525

15131526
protoResp, err := p.client.ConfigureStateStore(p.ctx, protoReq)
15141527
if err != nil {
15151528
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
15161529
return resp
15171530
}
1531+
resp.Capabilities = stateStoreServerCapabilitiesFromProto(protoResp.Capabilities)
1532+
logger.Trace("GRPCProvider.v6: ConfigureStateStore: received server capabilities", resp.Capabilities)
1533+
15181534
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
15191535
return resp
15201536
}
15211537

15221538
func (p *GRPCProvider) ReadStateBytes(r providers.ReadStateBytesRequest) (resp providers.ReadStateBytesResponse) {
15231539
logger.Trace("GRPCProvider.v6: ReadStateBytes")
15241540

1541+
// ReadStateBytes can be more sensitive to message sizes
1542+
// so we ensure it aligns with (the lower) terraform-plugin-go.
1543+
opts := grpc.MaxRecvMsgSizeCallOption{
1544+
MaxRecvMsgSize: grpcMaxMessageSize,
1545+
}
1546+
15251547
schema := p.GetProviderSchema()
15261548
if schema.Diagnostics.HasErrors() {
15271549
resp.Diagnostics = schema.Diagnostics
@@ -1544,14 +1566,15 @@ func (p *GRPCProvider) ReadStateBytes(r providers.ReadStateBytesRequest) (resp p
15441566
ctx, cancel := context.WithCancel(p.ctx)
15451567
defer cancel()
15461568

1547-
client, err := p.client.ReadStateBytes(ctx, protoReq)
1569+
client, err := p.client.ReadStateBytes(ctx, protoReq, opts)
15481570
if err != nil {
15491571
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
15501572
return resp
15511573
}
15521574

15531575
buf := &bytes.Buffer{}
15541576
var expectedTotalLength int
1577+
// TODO: Send warning if client misbehaves and uses (lower) chunk size that we didn't agree on
15551578
for {
15561579
chunk, err := client.Recv()
15571580
if err == io.EOF {
@@ -1613,6 +1636,18 @@ func (p *GRPCProvider) ReadStateBytes(r providers.ReadStateBytesRequest) (resp p
16131636
func (p *GRPCProvider) WriteStateBytes(r providers.WriteStateBytesRequest) (resp providers.WriteStateBytesResponse) {
16141637
logger.Trace("GRPCProvider.v6: WriteStateBytes")
16151638

1639+
// WriteStateBytes can be more sensitive to message sizes
1640+
// so we ensure it aligns with (the lower) terraform-plugin-go.
1641+
opts := grpc.MaxSendMsgSizeCallOption{
1642+
MaxSendMsgSize: grpcMaxMessageSize,
1643+
}
1644+
1645+
chunkSize, ok := p.stateChunkSize[r.TypeName]
1646+
if !ok {
1647+
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Unable to determine chunk size for provider %s; this is a bug in Terraform - please report it", r.TypeName))
1648+
return resp
1649+
}
1650+
16161651
schema := p.GetProviderSchema()
16171652
if schema.Diagnostics.HasErrors() {
16181653
resp.Diagnostics = schema.Diagnostics
@@ -1630,10 +1665,7 @@ func (p *GRPCProvider) WriteStateBytes(r providers.WriteStateBytesRequest) (resp
16301665
ctx, cancel := context.WithCancel(p.ctx)
16311666
defer cancel()
16321667

1633-
// TODO: Configurable chunk size
1634-
chunkSize := 4 * 1_000_000 // 4MB
1635-
1636-
client, err := p.client.WriteStateBytes(ctx)
1668+
client, err := p.client.WriteStateBytes(ctx, opts)
16371669
if err != nil {
16381670
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
16391671
return resp
@@ -1683,6 +1715,15 @@ func (p *GRPCProvider) WriteStateBytes(r providers.WriteStateBytesRequest) (resp
16831715
return resp
16841716
}
16851717

1718+
func (p *GRPCProvider) SetStateStoreChunkSize(typeName string, size int) {
1719+
p.mu.Lock()
1720+
defer p.mu.Unlock()
1721+
if p.stateChunkSize == nil {
1722+
p.stateChunkSize = make(map[string]int, 1)
1723+
}
1724+
p.stateChunkSize[typeName] = size
1725+
}
1726+
16861727
func (p *GRPCProvider) GetStates(r providers.GetStatesRequest) (resp providers.GetStatesResponse) {
16871728
logger.Trace("GRPCProvider.v6: GetStates")
16881729

@@ -1936,3 +1977,15 @@ func clientCapabilitiesToProto(c providers.ClientCapabilities) *proto6.ClientCap
19361977
WriteOnlyAttributesAllowed: c.WriteOnlyAttributesAllowed,
19371978
}
19381979
}
1980+
1981+
func stateStoreClientCapabilitiesToProto(c providers.StateStoreClientCapabilities) *proto6.StateStoreClientCapabilities {
1982+
return &proto6.StateStoreClientCapabilities{
1983+
ChunkSize: c.ChunkSize,
1984+
}
1985+
}
1986+
1987+
func stateStoreServerCapabilitiesFromProto(c *proto6.StateStoreServerCapabilities) providers.StateStoreServerCapabilities {
1988+
return providers.StateStoreServerCapabilities{
1989+
ChunkSize: c.ChunkSize,
1990+
}
1991+
}

internal/providers/provider.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ type Interface interface {
146146
Close() error
147147
}
148148

149+
type StateStoreChunkSizeSetter interface {
150+
SetStateStoreChunkSize(typeName string, size int)
151+
}
152+
149153
// GetProviderSchemaResponse is the return type for GetProviderSchema, and
150154
// should only be used when handling a value for that method. The handling of
151155
// of schemas in any other context should always use ProviderSchema, so that
@@ -839,11 +843,23 @@ type ConfigureStateStoreRequest struct {
839843

840844
// Config is the configuration value to configure the store with.
841845
Config cty.Value
846+
847+
Capabilities StateStoreClientCapabilities
848+
}
849+
850+
type StateStoreClientCapabilities struct {
851+
ChunkSize int64
842852
}
843853

844854
type ConfigureStateStoreResponse struct {
845855
// Diagnostics contains any warnings or errors from the method call.
846856
Diagnostics tfdiags.Diagnostics
857+
858+
Capabilities StateStoreServerCapabilities
859+
}
860+
861+
type StateStoreServerCapabilities struct {
862+
ChunkSize int64
847863
}
848864

849865
type ReadStateBytesRequest struct {

0 commit comments

Comments
 (0)