Skip to content
Draft
5 changes: 4 additions & 1 deletion config/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ type FunctionEngine interface {
// FunctionDeployment describes the code and runtime contract a FunctionEngine
// must be able to execute.
type FunctionDeployment struct {
Name string // function name (the Invoke/Remove key)
// Name is the Invoke/Remove key: the function name. Under AsyncSettle an
// AWS Lambda code update deploys to "<name>.update-<id>" instead, so the
// previous deployment keeps serving invocations until the update completes.
Name string
Runtime string // e.g. "python3.12", "nodejs20.x"
Handler string // e.g. "main.handler" (file.function)
Code []byte // deployment package (a .zip archive)
Expand Down
13 changes: 13 additions & 0 deletions internal/settle/settle.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ const (
// longer than the other phases so a real CLI or SDK client can observe a
// running job and call TerminateJob on it before it finishes.
DefaultBatchJobRunDwell = 5 * time.Second

DefaultLambdaCreateSettle = 1 * time.Second // Lambda function State Pending->Active
DefaultLambdaUpdateSettle = 1 * time.Second // Lambda LastUpdateStatus InProgress->Successful
)

// Window is a read-time overlay describing a resource still settling into its
Expand Down Expand Up @@ -198,6 +201,16 @@ func (s *Set) Settled(id string, now time.Time) bool {
return true
}

// Window returns id's window, or the inactive zero Window when id has none. It
// lets a dependent resource share the exact deadline of the window it is tied to
// (for example a Lambda version cut during an update settles with that update).
func (s *Set) Window(id string) Window {
s.mu.RLock()
defer s.mu.RUnlock()

return s.m[id]
}

// Clear drops id's window (on delete, or when a terminal state is reached and
// the window is no longer needed). Idempotent.
func (s *Set) Clear(id string) {
Expand Down
58 changes: 58 additions & 0 deletions providers/aws/lambda/account_settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package lambda

import (
"context"

"github.com/stackshy/cloudemu/v2/services/serverless/driver"
)

// AWS Lambda default account limits, in bytes, from the Lambda quotas page
// (https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html):
// 300 GB of code storage for .zip functions and layers, a 50 MB zipped
// deployment package uploaded directly, and 250 MB unzipped including layers.
// The concurrency budget is accountConcurrentExecutionLimit.
const (
accountTotalCodeSizeLimit int64 = 300 * 1024 * 1024 * 1024
accountCodeSizeZipped int64 = 50 * 1024 * 1024
accountCodeSizeUnzipped int64 = 250 * 1024 * 1024
)

// GetAccountSettings reports the account's Lambda limits and current usage
// (GetAccountSettings). UnreservedConcurrentExecutions is the concurrency pool
// minus every function's reserved concurrency; FunctionCount counts functions,
// and the used TotalCodeSize sums the deployment packages of every function's
// $LATEST and published versions plus every layer version, since "each
// function version and layer version consumes storage". It is an AWS-only
// capability the Lambda server handler type-asserts for.
func (m *Mock) GetAccountSettings(_ context.Context) (*driver.AccountSettings, error) {
var (
functions int64
codeSize int64
)

all := m.funcs.All()
for name := range all {
functions++
codeSize += all[name].info.CodeSize

for _, v := range all[name].versions {
codeSize += v.codeSize
}
}

for _, ld := range m.layers.All() {
for _, lv := range ld.versions.All() {
codeSize += lv.ContentSize
}
}

return &driver.AccountSettings{
TotalCodeSizeLimit: accountTotalCodeSizeLimit,
CodeSizeUnzipped: accountCodeSizeUnzipped,
CodeSizeZipped: accountCodeSizeZipped,
ConcurrentExecutions: accountConcurrentExecutionLimit,
UnreservedConcurrentExecutions: accountConcurrentExecutionLimit - m.sumReservedConcurrencyExcept(""),
FunctionCount: functions,
TotalCodeSizeUsed: codeSize,
}, nil
}
71 changes: 71 additions & 0 deletions providers/aws/lambda/alias_notfound_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package lambda

import (
"context"
"testing"

cerrors "github.com/stackshy/cloudemu/v2/errors"
"github.com/stackshy/cloudemu/v2/services/serverless/driver"
)

// TestAliasAndVersionNotFoundParity: UpdateAlias on a missing alias uses real
// Lambda's "Alias not found: <function ARN>:<alias>" text; DeleteAlias of a
// missing alias and DeleteFunction of a missing numbered version succeed
// without changing anything, while a missing function is still NotFound.
func TestAliasAndVersionNotFoundParity(t *testing.T) {
ctx := context.Background()
m := newTestMock()

if _, err := m.CreateFunction(ctx, defaultFuncConfig()); err != nil {
t.Fatal(err)
}

if _, err := m.PublishVersion(ctx, "my-func", ""); err != nil {
t.Fatal(err)
}

if _, err := m.CreateAlias(ctx, driver.AliasConfig{FunctionName: "my-func", Name: "live", FunctionVersion: "1"}); err != nil {
t.Fatal(err)
}

_, err := m.UpdateAlias(ctx, driver.AliasConfig{FunctionName: "my-func", Name: "nope", FunctionVersion: "1"})
if !cerrors.IsNotFound(err) {
t.Fatalf("UpdateAlias(missing) err = %v, want NotFound", err)
}

if got, want := cerrors.Message(err),
"Alias not found: arn:aws:lambda:us-east-1:123456789012:function:my-func:nope"; got != want {
t.Errorf("UpdateAlias message = %q, want %q", got, want)
}

if err := m.DeleteAlias(ctx, "my-func", "nope"); err != nil {
t.Errorf("DeleteAlias(missing alias) err = %v, want nil", err)
}

if _, err := m.GetAlias(ctx, "my-func", "live"); err != nil {
t.Errorf("DeleteAlias(missing alias) touched another alias: %v", err)
}

if err := m.DeleteAlias(ctx, "ghost", "live"); !cerrors.IsNotFound(err) {
t.Errorf("DeleteAlias(missing function) err = %v, want NotFound", err)
}

if err := m.DeleteVersion(ctx, "my-func", "99"); err != nil {
t.Errorf("DeleteVersion(missing version) err = %v, want nil", err)
}

if err := m.DeleteVersion(ctx, "ghost", "1"); !cerrors.IsNotFound(err) {
t.Errorf("DeleteVersion(missing function) err = %v, want NotFound", err)
}

err = m.DeleteVersion(ctx, "my-func", "not-an-alias")
if !cerrors.IsNotFound(err) {
t.Fatalf("DeleteVersion(unknown non-numeric qualifier) err = %v, want NotFound", err)
}

// Worded like the other not-found errors: the qualified function ARN.
if got, want := cerrors.Message(err),
"Function not found: arn:aws:lambda:us-east-1:123456789012:function:my-func:not-an-alias"; got != want {
t.Errorf("DeleteVersion(unknown non-numeric qualifier) message = %q, want %q", got, want)
}
}
33 changes: 20 additions & 13 deletions providers/aws/lambda/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (
)

// CreateAlias creates a new alias pointing to a specific function version.
func (m *Mock) CreateAlias(_ context.Context, cfg driver.AliasConfig) (*driver.Alias, error) {
func (m *Mock) CreateAlias(ctx context.Context, cfg driver.AliasConfig) (*driver.Alias, error) {
m.mu.Lock()
defer m.mu.Unlock()

fd, ok := m.funcs.Get(cfg.FunctionName)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "function %s not found", cfg.FunctionName)
return nil, m.errFunctionNotFound(ctx, cfg.FunctionName, "")
}

if fd.aliases.Has(cfg.Name) {
Expand Down Expand Up @@ -56,18 +56,19 @@ func (m *Mock) CreateAlias(_ context.Context, cfg driver.AliasConfig) (*driver.A
}

// UpdateAlias updates an existing alias configuration.
func (m *Mock) UpdateAlias(_ context.Context, cfg driver.AliasConfig) (*driver.Alias, error) {
func (m *Mock) UpdateAlias(ctx context.Context, cfg driver.AliasConfig) (*driver.Alias, error) {
m.mu.Lock()
defer m.mu.Unlock()

fd, ok := m.funcs.Get(cfg.FunctionName)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "function %s not found", cfg.FunctionName)
return nil, m.errFunctionNotFound(ctx, cfg.FunctionName, "")
}

ad, ok := fd.aliases.Get(cfg.Name)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "alias %s not found", cfg.Name)
// Real Lambda's UpdateAlias text names the alias-qualified function ARN.
return nil, cerrors.Newf(cerrors.NotFound, "Alias not found: %s:%s", fd.info.ARN, cfg.Name)
}

// ad is a shared pointer held in the store; guard the read-validate-mutate of
Expand Down Expand Up @@ -121,17 +122,22 @@ func (m *Mock) UpdateAlias(_ context.Context, cfg driver.AliasConfig) (*driver.A
// provisioned concurrency config and event invoke config. Real Lambda keeps
// these as sub-resources of the alias ARN, so a later alias with the same name
// starts with none of them instead of inheriting the old grants.
func (m *Mock) DeleteAlias(_ context.Context, functionName, aliasName string) error {
//
// Deleting an alias the function does not have succeeds without changing
// anything, as real Lambda does (HTTP 204; ResourceNotFoundException is not in
// the DeleteAlias error list). A missing function is still
// ResourceNotFoundException.
func (m *Mock) DeleteAlias(ctx context.Context, functionName, aliasName string) error {
m.mu.Lock()
defer m.mu.Unlock()

fd, ok := m.funcs.Get(functionName)
if !ok {
return cerrors.Newf(cerrors.NotFound, "function %s not found", functionName)
return m.errFunctionNotFound(ctx, functionName, "")
}

if !fd.aliases.Has(aliasName) {
return cerrors.Newf(cerrors.NotFound, "alias %s not found", aliasName)
return nil
}

fd.aliases.Delete(aliasName)
Expand Down Expand Up @@ -166,15 +172,16 @@ func withoutKey[V any](src map[string]V, key string) map[string]V {
}

// GetAlias retrieves a specific alias for a function.
func (m *Mock) GetAlias(_ context.Context, functionName, aliasName string) (*driver.Alias, error) {
func (m *Mock) GetAlias(ctx context.Context, functionName, aliasName string) (*driver.Alias, error) {
fd, ok := m.funcs.Get(functionName)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "function %s not found", functionName)
return nil, m.errFunctionNotFound(ctx, functionName, "")
}

ad, ok := fd.aliases.Get(aliasName)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "alias %s not found", aliasName)
// Real Lambda's GetAlias text names the alias-qualified function ARN.
return nil, cerrors.Newf(cerrors.NotFound, "Cannot find alias arn: %s:%s", fd.info.ARN, aliasName)
}

ad.mu.Lock()
Expand All @@ -185,10 +192,10 @@ func (m *Mock) GetAlias(_ context.Context, functionName, aliasName string) (*dri
}

// ListAliases returns all aliases for a function.
func (m *Mock) ListAliases(_ context.Context, functionName string) ([]driver.Alias, error) {
func (m *Mock) ListAliases(ctx context.Context, functionName string) ([]driver.Alias, error) {
fd, ok := m.funcs.Get(functionName)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "function %s not found", functionName)
return nil, m.errFunctionNotFound(ctx, functionName, "")
}

all := fd.aliases.All()
Expand Down
28 changes: 16 additions & 12 deletions providers/aws/lambda/awsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package lambda
import (
"context"

cerrors "github.com/stackshy/cloudemu/v2/errors"
"github.com/stackshy/cloudemu/v2/services/serverless/driver"
)

Expand All @@ -16,13 +15,13 @@ import (
// surface.
//
//nolint:gocritic // hugeParam: interface method signature cannot be changed.
func (m *Mock) SetFunctionAWSConfig(_ context.Context, name string, cfg driver.AWSFunctionConfig, create bool) error {
func (m *Mock) SetFunctionAWSConfig(ctx context.Context, name string, cfg driver.AWSFunctionConfig, create bool) error {
m.mu.Lock()
defer m.mu.Unlock()

fd, ok := m.funcs.Get(name)
if !ok {
return cerrors.Newf(cerrors.NotFound, "function %s not found", name)
return m.errFunctionNotFound(ctx, name, "")
}

if create {
Expand Down Expand Up @@ -78,18 +77,23 @@ func overlayAWSConfig(dst *driver.AWSFunctionConfig, cfg driver.AWSFunctionConfi

// GetFunctionAWSConfig returns a copy of the stored AWS-only settings for a
// function.
func (m *Mock) GetFunctionAWSConfig(_ context.Context, name string) (driver.AWSFunctionConfig, error) {
func (m *Mock) GetFunctionAWSConfig(ctx context.Context, name string) (driver.AWSFunctionConfig, error) {
fd, ok := m.funcs.Get(name)
if !ok {
return driver.AWSFunctionConfig{}, cerrors.Newf(cerrors.NotFound, "function %s not found", name)
return driver.AWSFunctionConfig{}, m.errFunctionNotFound(ctx, name, "")
}

return cloneAWSConfig(&fd.awsConfig), nil
}

// cloneAWSConfig deep-copies a function's AWS-only settings.
func cloneAWSConfig(c *driver.AWSFunctionConfig) driver.AWSFunctionConfig {
return driver.AWSFunctionConfig{
VPCConfig: cloneVPCConfig(fd.awsConfig.VPCConfig),
DeadLetterConfig: cloneDeadLetterConfig(fd.awsConfig.DeadLetterConfig),
TracingConfig: cloneTracingConfig(fd.awsConfig.TracingConfig),
Architectures: cloneArchitectures(fd.awsConfig.Architectures),
EphemeralStorage: cloneEphemeralStorage(fd.awsConfig.EphemeralStorage),
Layers: cloneLayers(fd.awsConfig.Layers),
}, nil
VPCConfig: cloneVPCConfig(c.VPCConfig),
DeadLetterConfig: cloneDeadLetterConfig(c.DeadLetterConfig),
TracingConfig: cloneTracingConfig(c.TracingConfig),
Architectures: cloneArchitectures(c.Architectures),
EphemeralStorage: cloneEphemeralStorage(c.EphemeralStorage),
Layers: cloneLayers(c.Layers),
}
}
Loading
Loading