Skip to content

Commit

Permalink
Merge branch 'main' into tt-1936-seth-simulated-backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofel committed Jan 22, 2025
2 parents e6d9102 + 8e7f8a1 commit e076fb9
Show file tree
Hide file tree
Showing 19 changed files with 136 additions and 29 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/framework-golden-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ jobs:
config: smoke.toml
count: 1
timeout: 10m
- name: TestSmoke
config: smoke_limited_resources.toml
count: 1
timeout: 10m
- name: TestSuiSmoke
config: smoke_sui.toml
count: 1
Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [Debugging Tests](framework/components/debug.md)
- [Components Cleanup](framework/components/cleanup.md)
- [Components Caching](framework/components/caching.md)
- [Components Resources](framework/components/resources.md)
- [Mocking Services](framework/components/mocking.md)
- [Copying Files](framework/copying_files.md)
- [External Environment](framework/components/external.md)
Expand Down
33 changes: 33 additions & 0 deletions book/src/framework/components/resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Components Resources

You can use `resources` to limit containers CPU/Memory for `NodeSet`, `Blockchain` and `PostgreSQL` components.

```toml
[blockchain_a.resources]
cpus = 0.5
memory_mb = 1048

[nodeset.db.resources]
cpus = 2
memory_mb = 2048

[nodeset.node_specs.node.resources]
cpus = 1
memory_mb = 1048
```

Read more about resource constraints [here](https://docs.docker.com/engine/containers/resource_constraints/).

We are using `cpu-period` and `cpu-quota` for simplicity, and because it's working with an arbitrary amount of containers, it is absolute.

How quota and period works:

- To allocate `1 CPU`, we set `CPUQuota = 100000` and `CPUPeriod = 100000` (1 full period).
- To allocate `0.5 CPU`, we set `CPUQuota = 50000` and `CPUPeriod = 100000`.
- To allocate `2 CPUs`, we set `CPUQuota = 200000` and `CPUPeriod = 100000`.

Read more about [CFS](https://engineering.squarespace.com/blog/2017/understanding-linux-container-scheduling).

When the `resources.memory_mb` key is not empty, we disable swap, ensuring the container goes OOM when memory is exhausted, allowing for more precise detection of sudden memory spikes.

Full configuration [example](https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/framework/examples/myproject/smoke_limited_resources.toml)
1 change: 1 addition & 0 deletions framework/.changeset/v0.4.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Set cgroups resource for containers
1 change: 1 addition & 0 deletions framework/components/blockchain/anvil.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func newAnvil(in *Input) (*Output, error) {
ExposedPorts: []string{bindPort},
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
},
Networks: []string{framework.DefaultNetworkName},
NetworkAliases: map[string][]string{
Expand Down
1 change: 1 addition & 0 deletions framework/components/blockchain/aptos.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func newAptos(in *Input) (*Output, error) {
},
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
},
ImagePlatform: "linux/amd64",
Cmd: []string{
Expand Down
1 change: 1 addition & 0 deletions framework/components/blockchain/besu.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func newBesu(in *Input) (*Output, error) {
Labels: framework.DefaultTCLabels(),
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPortWs, bindPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
},
WaitingFor: wait.ForListeningPort(nat.Port(in.Port)).WithStartupTimeout(15 * time.Second).WithPollInterval(200 * time.Millisecond),
Cmd: entryPoint,
Expand Down
4 changes: 3 additions & 1 deletion framework/components/blockchain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package blockchain

import (
"fmt"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/testcontainers/testcontainers-go"
)

Expand All @@ -25,7 +26,8 @@ type Input struct {
// programs to deploy on solana-test-validator start
// a map of program name to program id
// there needs to be a matching .so file in contracts_dir
SolanaPrograms map[string]string `toml:"solana_programs"`
SolanaPrograms map[string]string `toml:"solana_programs"`
ContainerResources *framework.ContainerResources `toml:"resources"`
}

// Output is a blockchain network output, ChainID and one or more nodes that forms the network
Expand Down
1 change: 1 addition & 0 deletions framework/components/blockchain/geth.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ func newGeth(in *Input) (*Output, error) {
ExposedPorts: []string{bindPort},
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
h.Mounts = append(h.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: keystoreDir,
Expand Down
1 change: 1 addition & 0 deletions framework/components/blockchain/solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func newSolana(in *Input) (*Output, error) {
WithPollInterval(100 * time.Millisecond),
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort, wsBindPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
h.Mounts = append(h.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: contractsDir,
Expand Down
1 change: 1 addition & 0 deletions framework/components/blockchain/sui.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func newSui(in *Input) (*Output, error) {
},
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort, DefaultFaucetPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
},
ImagePlatform: "linux/amd64",
Env: map[string]string{
Expand Down
1 change: 1 addition & 0 deletions framework/components/blockchain/tron.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func newTron(in *Input) (*Output, error) {
Labels: framework.DefaultTCLabels(),
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort)
framework.ResourceLimitsFunc(h, in.ContainerResources)
},
WaitingFor: wait.ForLog("Mnemonic").WithPollInterval(200 * time.Millisecond).WithStartupTimeout(1 * time.Minute),
Files: []testcontainers.ContainerFile{
Expand Down
32 changes: 17 additions & 15 deletions framework/components/clnode/clnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,22 @@ type Input struct {

// NodeInput is CL nod container inputs
type NodeInput struct {
Image string `toml:"image" validate:"required"`
Name string `toml:"name"`
DockerFilePath string `toml:"docker_file"`
DockerContext string `toml:"docker_ctx"`
PullImage bool `toml:"pull_image"`
CapabilitiesBinaryPaths []string `toml:"capabilities"`
CapabilityContainerDir string `toml:"capabilities_container_dir"`
TestConfigOverrides string `toml:"test_config_overrides"`
UserConfigOverrides string `toml:"user_config_overrides"`
TestSecretsOverrides string `toml:"test_secrets_overrides"`
UserSecretsOverrides string `toml:"user_secrets_overrides"`
HTTPPort int `toml:"port"`
P2PPort int `toml:"p2p_port"`
CustomPorts []string `toml:"custom_ports"`
DebuggerPort int `toml:"debugger_port"`
Image string `toml:"image" validate:"required"`
Name string `toml:"name"`
DockerFilePath string `toml:"docker_file"`
DockerContext string `toml:"docker_ctx"`
PullImage bool `toml:"pull_image"`
CapabilitiesBinaryPaths []string `toml:"capabilities"`
CapabilityContainerDir string `toml:"capabilities_container_dir"`
TestConfigOverrides string `toml:"test_config_overrides"`
UserConfigOverrides string `toml:"user_config_overrides"`
TestSecretsOverrides string `toml:"test_secrets_overrides"`
UserSecretsOverrides string `toml:"user_secrets_overrides"`
HTTPPort int `toml:"port"`
P2PPort int `toml:"p2p_port"`
CustomPorts []string `toml:"custom_ports"`
DebuggerPort int `toml:"debugger_port"`
ContainerResources *framework.ContainerResources `toml:"resources"`
}

// Output represents Chainlink node output, nodes and databases connection URLs
Expand Down Expand Up @@ -247,6 +248,7 @@ func newNode(in *Input, pgOut *postgres.Output) (*NodeOut, error) {
if in.Node.HTTPPort != 0 && in.Node.P2PPort != 0 {
req.HostConfigModifier = func(h *container.HostConfig) {
h.PortBindings = portBindings
framework.ResourceLimitsFunc(h, in.Node.ContainerResources)
}
}
files := []tc.ContainerFile{
Expand Down
18 changes: 10 additions & 8 deletions framework/components/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ const (
)

type Input struct {
Image string `toml:"image" validate:"required"`
Port int `toml:"port"`
Name string `toml:"name"`
VolumeName string `toml:"volume_name"`
Databases int `toml:"databases"`
JDDatabase bool `toml:"jd_database"`
PullImage bool `toml:"pull_image"`
Out *Output `toml:"out"`
Image string `toml:"image" validate:"required"`
Port int `toml:"port"`
Name string `toml:"name"`
VolumeName string `toml:"volume_name"`
Databases int `toml:"databases"`
JDDatabase bool `toml:"jd_database"`
PullImage bool `toml:"pull_image"`
ContainerResources *framework.ContainerResources `toml:"resources"`
Out *Output `toml:"out"`
}

type Output struct {
Expand Down Expand Up @@ -133,6 +134,7 @@ func NewPostgreSQL(in *Input) (*Output, error) {
},
},
}
framework.ResourceLimitsFunc(h, in.ContainerResources)
}
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Expand Down
1 change: 1 addition & 0 deletions framework/components/simple_node_set/node_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func sharedDBSetup(in *Input, bcOut *blockchain.Output) (*Output, error) {
UserConfigOverrides: in.NodeSpecs[overrideIdx].Node.UserConfigOverrides,
TestSecretsOverrides: in.NodeSpecs[overrideIdx].Node.TestSecretsOverrides,
UserSecretsOverrides: in.NodeSpecs[overrideIdx].Node.UserSecretsOverrides,
ContainerResources: in.NodeSpecs[overrideIdx].Node.ContainerResources,
},
}

Expand Down
25 changes: 25 additions & 0 deletions framework/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,28 @@ func ExecContainer(containerName string, command []string) (string, error) {
L.Info().Str("Output", string(output)).Msg("Command output")
return string(output), nil
}

type ContainerResources struct {
CPUs float64 `toml:"cpus" validate:"gte=0"`
MemoryMb uint `toml:"memory_mb"`
}

// ResourceLimitsFunc returns a function to configure container resources based on the human-readable CPUs and memory in Mb
func ResourceLimitsFunc(h *container.HostConfig, resources *ContainerResources) {
if resources == nil {
return
}
if resources.MemoryMb > 0 {
h.Memory = int64(resources.MemoryMb) * 1024 * 1024 // Memory in Mb
h.MemoryReservation = int64(resources.MemoryMb) * 1024 * 1024 // Total memory that can be reserved (soft) in Mb
// https://docs.docker.com/engine/containers/resource_constraints/ if both values are equal swap is off, read the docs
h.MemorySwap = h.Memory
}
if resources.CPUs > 0 {
// Set CPU limits using CPUQuota and CPUPeriod
// we don't use runtime.NumCPU or docker API to get CPUs because h.CPUShares is relative to amount of containers you run
// CPUPeriod and CPUQuota are absolute and easier to control
h.CPUPeriod = 100000 // Default period (100ms)
h.CPUQuota = int64(resources.CPUs * 100000) // Quota in microseconds (e.g., 0.5 CPUs = 50000)
}
}
6 changes: 2 additions & 4 deletions framework/examples/myproject/smoke.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@

[blockchain_a]
# choose "anvil", "geth" or "besu"
# use docker_cmd_params = ["-b", "1"] for "anvil"
type = "anvil"
docker_cmd_params = ["-b", "1"]
type = "anvil"

[data_provider]
port = 9111
Expand All @@ -18,4 +16,4 @@
[[nodeset.node_specs]]

[nodeset.node_specs.node]
image = "public.ecr.aws/chainlink/chainlink:v2.17.0"
image = "public.ecr.aws/chainlink/chainlink:v2.17.0"
31 changes: 31 additions & 0 deletions framework/examples/myproject/smoke_limited_resources.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

[blockchain_a]
docker_cmd_params = ["-b", "1"]
type = "anvil"

[blockchain_a.resources]
cpus = 1
memory_mb = 1048

[data_provider]
port = 9111

[nodeset]
nodes = 5
override_mode = "all"

[nodeset.db]
image = "postgres:12.0"

[nodeset.db.resources]
cpus = 1
memory_mb = 1048

[[nodeset.node_specs]]

[nodeset.node_specs.node]
image = "public.ecr.aws/chainlink/chainlink:v2.17.0"

[nodeset.node_specs.node.resources]
cpus = 1
memory_mb = 1048
2 changes: 1 addition & 1 deletion seth/client_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ func TestConfig_EthClient_DoesntAllowRpcUrl(t *testing.T) {
require.Nil(t, client, "expected client to be nil")
}

func TestConfig_EthClient(t *testing.T) {
func TestExtraConfig_EthClient(t *testing.T) {
builder := seth.NewClientBuilder()

ethclient, err := ethclient.Dial("ws://localhost:8545")
Expand Down

0 comments on commit e076fb9

Please sign in to comment.