diff --git a/backend/controller/controller.go b/backend/controller/controller.go index f17fc50eee..89e5b2ce68 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -22,7 +22,6 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/jellydator/ttlcache/v3" "github.com/jpillora/backoff" - "github.com/oklog/ulid/v2" "golang.org/x/exp/maps" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" @@ -55,7 +54,7 @@ type Config struct { ConsoleURL *url.URL `help:"The public URL of the console (for CORS)." env:"FTL_CONTROLLER_CONSOLE_URL"` AllowOrigins []*url.URL `help:"Allow CORS requests to ingress endpoints from these origins." env:"FTL_CONTROLLER_ALLOW_ORIGIN"` ContentTime time.Time `help:"Time to use for console resource timestamps." default:"${timestamp=1970-01-01T00:00:00Z}"` - Key model.ControllerKey `help:"Controller key (auto)." placeholder:"C" default:"C00000000000000000000000000"` + Key model.ControllerKey `help:"Controller key (auto)."` DSN string `help:"DAL DSN." default:"postgres://localhost:54320/ftl?sslmode=disable&user=postgres&password=secret" env:"FTL_CONTROLLER_DSN"` RunnerTimeout time.Duration `help:"Runner heartbeat timeout." default:"10s"` DeploymentReservationTimeout time.Duration `help:"Deployment reservation timeout." default:"120s"` @@ -152,9 +151,10 @@ type Service struct { } func New(ctx context.Context, db *dal.DAL, config Config, runnerScaling scaling.RunnerScaling) (*Service, error) { + var zero model.ControllerKey key := config.Key - if config.Key.ULID() == (ulid.ULID{}) { - key = model.NewControllerKey() + if config.Key == zero { + key = model.NewControllerKey(config.Bind.Hostname(), config.Bind.Port()) } config.SetDefaults() svc := &Service{ diff --git a/backend/controller/dal/dal.go b/backend/controller/dal/dal.go index c2000d09f2..9c9df1924d 100644 --- a/backend/controller/dal/dal.go +++ b/backend/controller/dal/dal.go @@ -91,8 +91,9 @@ func runnerFromDB(row sql.GetRunnerRow) Runner { if err := json.Unmarshal(row.Labels, &attrs); err != nil { return Runner{} } + return Runner{ - Key: model.RunnerKey(row.RunnerKey), + Key: row.RunnerKey, Endpoint: row.Endpoint, State: RunnerState(row.State), Deployment: deployment, @@ -308,8 +309,9 @@ func (d *DAL) GetStatus( if err := json.Unmarshal(in.Labels, &attrs); err != nil { return Runner{}, fmt.Errorf("invalid attributes JSON for runner %s: %w", in.RunnerKey, err) } + return Runner{ - Key: model.RunnerKey(in.RunnerKey), + Key: in.RunnerKey, Endpoint: in.Endpoint, State: RunnerState(in.State), Deployment: deployment, @@ -335,7 +337,7 @@ func (d *DAL) GetStatus( Routes: slices.Map(routes, func(row sql.GetRoutingTableRow) Route { return Route{ Module: row.ModuleName.MustGet(), - Runner: model.RunnerKey(row.RunnerKey), + Runner: row.RunnerKey, Deployment: row.DeploymentName, Endpoint: row.Endpoint, } @@ -354,8 +356,9 @@ func (d *DAL) GetRunnersForDeployment(ctx context.Context, deployment model.Depl if err := json.Unmarshal(row.Labels, &attrs); err != nil { return nil, fmt.Errorf("invalid attributes JSON for runner %d: %w", row.ID, err) } + runners = append(runners, Runner{ - Key: model.RunnerKey(row.Key), + Key: row.Key, Endpoint: row.Endpoint, State: RunnerState(row.State), Deployment: optional.Some(deployment), @@ -502,7 +505,7 @@ func (d *DAL) UpsertRunner(ctx context.Context, runner Runner) error { return fmt.Errorf("%s: %w", "failed to JSON encode runner labels", err) } deploymentID, err := d.db.UpsertRunner(ctx, sql.UpsertRunnerParams{ - Key: sql.Key(runner.Key), + Key: runner.Key, Endpoint: runner.Endpoint, State: sql.RunnerState(runner.State), DeploymentName: pgDeploymentName, @@ -511,9 +514,6 @@ func (d *DAL) UpsertRunner(ctx context.Context, runner Runner) error { if err != nil { return translatePGError(err) } - if err != nil { - return translatePGError(err) - } if runner.Deployment.Ok() && !deploymentID.Ok() { return fmt.Errorf("deployment %s not found", runner.Deployment) } @@ -534,7 +534,7 @@ func (d *DAL) KillStaleControllers(ctx context.Context, age time.Duration) (int6 // DeregisterRunner deregisters the given runner. func (d *DAL) DeregisterRunner(ctx context.Context, key model.RunnerKey) error { - count, err := d.db.DeregisterRunner(ctx, sql.Key(key)) + count, err := d.db.DeregisterRunner(ctx, key) if err != nil { return translatePGError(err) } @@ -574,11 +574,12 @@ func (d *DAL) ReserveRunnerForDeployment(ctx context.Context, deployment model.D cancel() return nil, fmt.Errorf("failed to JSON decode labels for runner %d: %w", runner.ID, err) } + return &postgresClaim{ cancel: cancel, tx: tx, runner: Runner{ - Key: model.RunnerKey(runner.Key), + Key: runner.Key, Endpoint: runner.Endpoint, State: RunnerState(runner.State), Deployment: optional.Some(deployment), @@ -628,7 +629,7 @@ func (d *DAL) SetDeploymentReplicas(ctx context.Context, key model.DeploymentNam } err = tx.InsertDeploymentUpdatedEvent(ctx, sql.InsertDeploymentUpdatedEventParams{ - DeploymentName: key.String(), + DeploymentName: string(key), MinReplicas: int32(minReplicas), PrevMinReplicas: deployment.MinReplicas, }) @@ -766,8 +767,9 @@ func (d *DAL) GetProcessList(ctx context.Context) ([]Process, error) { if err := json.Unmarshal(row.RunnerLabels, &labels); err != nil { return Process{}, fmt.Errorf("invalid labels JSON for runner %s: %w", row.RunnerKey, err) } + runner = optional.Some(ProcessRunner{ - Key: model.RunnerKey(row.RunnerKey.MustGet()), + Key: row.RunnerKey.MustGet(), Endpoint: endpoint, Labels: labels, }) @@ -812,8 +814,9 @@ func (d *DAL) GetIdleRunners(ctx context.Context, limit int, labels model.Labels if err != nil { return Runner{}, fmt.Errorf("%s: %w", "could not unmarshal labels", err) } + return Runner{ - Key: model.RunnerKey(row.Key), + Key: row.Key, Endpoint: row.Endpoint, State: RunnerState(row.State), Labels: labels, @@ -840,7 +843,7 @@ func (d *DAL) GetRoutingTable(ctx context.Context, modules []string) (map[string out[moduleName] = append(out[moduleName], Route{ Module: moduleName, Deployment: route.DeploymentName, - Runner: model.RunnerKey(route.RunnerKey), + Runner: route.RunnerKey, Endpoint: route.Endpoint, }) } @@ -848,7 +851,7 @@ func (d *DAL) GetRoutingTable(ctx context.Context, modules []string) (map[string } func (d *DAL) GetRunnerState(ctx context.Context, runnerKey model.RunnerKey) (RunnerState, error) { - state, err := d.db.GetRunnerState(ctx, sql.Key(runnerKey)) + state, err := d.db.GetRunnerState(ctx, runnerKey) if err != nil { return "", translatePGError(err) } @@ -856,7 +859,7 @@ func (d *DAL) GetRunnerState(ctx context.Context, runnerKey model.RunnerKey) (Ru } func (d *DAL) GetRunner(ctx context.Context, runnerKey model.RunnerKey) (Runner, error) { - row, err := d.db.GetRunner(ctx, sql.Key(runnerKey)) + row, err := d.db.GetRunner(ctx, runnerKey) if err != nil { return Runner{}, translatePGError(err) } @@ -927,7 +930,7 @@ func (d *DAL) GetIngressRoutes(ctx context.Context, method string) ([]IngressRou } return slices.Map(routes, func(row sql.GetIngressRoutesRow) IngressRoute { return IngressRoute{ - Runner: model.RunnerKey(row.RunnerKey), + Runner: row.RunnerKey, Deployment: row.DeploymentName, Endpoint: row.Endpoint, Path: row.Path, diff --git a/backend/controller/dal/dal_test.go b/backend/controller/dal/dal_test.go index 8965893789..0acae29eee 100644 --- a/backend/controller/dal/dal_test.go +++ b/backend/controller/dal/dal_test.go @@ -86,7 +86,7 @@ func TestDAL(t *testing.T) { assert.Equal(t, []sha256.SHA256{misshingSHA}, missing) }) - runnerID := model.NewRunnerKey() + runnerID := model.NewRunnerKey("localhost", "8080") labels := map[string]any{"languages": []any{"go"}} t.Run("RegisterRunner", func(t *testing.T) { @@ -101,7 +101,7 @@ func TestDAL(t *testing.T) { t.Run("RegisterRunnerFailsOnDuplicate", func(t *testing.T) { err = dal.UpsertRunner(ctx, Runner{ - Key: model.NewRunnerKey(), + Key: model.NewRunnerKey("localhost", "8080"), Labels: labels, Endpoint: "http://localhost:8080", State: RunnerStateIdle, @@ -333,7 +333,7 @@ func TestDAL(t *testing.T) { }) t.Run("DeregisterRunnerFailsOnMissing", func(t *testing.T) { - err = dal.DeregisterRunner(ctx, model.NewRunnerKey()) + err = dal.DeregisterRunner(ctx, model.NewRunnerKey("localhost", "8080")) assert.IsError(t, err, ErrNotFound) }) } diff --git a/backend/controller/scaling/localscaling/local_scaling.go b/backend/controller/scaling/localscaling/local_scaling.go index e3d9e33585..98080d9a1c 100644 --- a/backend/controller/scaling/localscaling/local_scaling.go +++ b/backend/controller/scaling/localscaling/local_scaling.go @@ -27,6 +27,8 @@ type LocalScaling struct { portAllocator *bind.BindAllocator controllerAddresses []*url.URL + + prevRunnerSuffix int } func NewLocalScaling(portAllocator *bind.BindAllocator, controllerAddresses []*url.URL) (*LocalScaling, error) { @@ -40,6 +42,7 @@ func NewLocalScaling(portAllocator *bind.BindAllocator, controllerAddresses []*u runners: map[model.RunnerKey]context.CancelFunc{}, portAllocator: portAllocator, controllerAddresses: controllerAddresses, + prevRunnerSuffix: -1, }, nil } @@ -72,25 +75,28 @@ func (l *LocalScaling) SetReplicas(ctx context.Context, replicas int, idleRunner logger.Debugf("Adding %d replicas", replicasToAdd) for i := 0; i < replicasToAdd; i++ { - i := i - controllerEndpoint := l.controllerAddresses[len(l.runners)%len(l.controllerAddresses)] + + bind := l.portAllocator.Next() + keySuffix := l.prevRunnerSuffix + 1 + l.prevRunnerSuffix = keySuffix + config := runner.Config{ - Bind: l.portAllocator.Next(), + Bind: bind, ControllerEndpoint: controllerEndpoint, TemplateDir: templateDir(ctx), - Key: model.NewRunnerKey(), + Key: model.NewLocalRunnerKey(keySuffix), } - name := fmt.Sprintf("runner%d", i) + simpleName := fmt.Sprintf("runner%d", keySuffix) if err := kong.ApplyDefaults(&config, kong.Vars{ - "deploymentdir": filepath.Join(l.cacheDir, "ftl-runner", name, "deployments"), + "deploymentdir": filepath.Join(l.cacheDir, "ftl-runner", simpleName, "deployments"), "language": "go,kotlin", }); err != nil { return err } - runnerCtx := log.ContextWithLogger(ctx, logger.Scope(name)) + runnerCtx := log.ContextWithLogger(ctx, logger.Scope(simpleName)) runnerCtx, cancel := context.WithCancel(runnerCtx) l.runners[config.Key] = cancel diff --git a/backend/controller/scheduledtask/scheduledtask_test.go b/backend/controller/scheduledtask/scheduledtask_test.go index 310614c844..8b325d92c9 100644 --- a/backend/controller/scheduledtask/scheduledtask_test.go +++ b/backend/controller/scheduledtask/scheduledtask_test.go @@ -31,10 +31,10 @@ func TestCron(t *testing.T) { } controllers := []*controller{ - {controller: dal.Controller{Key: model.NewControllerKey()}}, - {controller: dal.Controller{Key: model.NewControllerKey()}}, - {controller: dal.Controller{Key: model.NewControllerKey()}}, - {controller: dal.Controller{Key: model.NewControllerKey()}}, + {controller: dal.Controller{Key: model.NewControllerKey("localhost", "8080")}}, + {controller: dal.Controller{Key: model.NewControllerKey("localhost", "8081")}}, + {controller: dal.Controller{Key: model.NewControllerKey("localhost", "8082")}}, + {controller: dal.Controller{Key: model.NewControllerKey("localhost", "8083")}}, } clock := clock.NewMock() diff --git a/backend/controller/sql/models.go b/backend/controller/sql/models.go index 28bb5fb2cd..b9ab06e1de 100644 --- a/backend/controller/sql/models.go +++ b/backend/controller/sql/models.go @@ -258,7 +258,7 @@ type Request struct { type Runner struct { ID int64 - Key Key + Key model.RunnerKey Created time.Time LastSeen time.Time ReservationTimeout NullTime diff --git a/backend/controller/sql/querier.go b/backend/controller/sql/querier.go index ed20a23e83..7aa57367f0 100644 --- a/backend/controller/sql/querier.go +++ b/backend/controller/sql/querier.go @@ -19,7 +19,7 @@ type Querier interface { CreateDeployment(ctx context.Context, name model.DeploymentName, moduleName string, schema []byte) error CreateIngressRequest(ctx context.Context, origin Origin, name string, sourceAddr string) error CreateIngressRoute(ctx context.Context, arg CreateIngressRouteParams) error - DeregisterRunner(ctx context.Context, key Key) (int64, error) + DeregisterRunner(ctx context.Context, key model.RunnerKey) (int64, error) ExpireRunnerReservations(ctx context.Context) (int64, error) GetActiveDeploymentSchemas(ctx context.Context) ([]GetActiveDeploymentSchemasRow, error) GetActiveDeployments(ctx context.Context, all bool) ([]GetActiveDeploymentsRow, error) @@ -44,10 +44,10 @@ type Querier interface { GetModulesByID(ctx context.Context, ids []int64) ([]Module, error) GetProcessList(ctx context.Context) ([]GetProcessListRow, error) // Retrieve routing information for a runner. - GetRouteForRunner(ctx context.Context, key Key) (GetRouteForRunnerRow, error) + GetRouteForRunner(ctx context.Context, key model.RunnerKey) (GetRouteForRunnerRow, error) GetRoutingTable(ctx context.Context, modules []string) ([]GetRoutingTableRow, error) - GetRunner(ctx context.Context, key Key) (GetRunnerRow, error) - GetRunnerState(ctx context.Context, key Key) (RunnerState, error) + GetRunner(ctx context.Context, key model.RunnerKey) (GetRunnerRow, error) + GetRunnerState(ctx context.Context, key model.RunnerKey) (RunnerState, error) GetRunnersForDeployment(ctx context.Context, name model.DeploymentName) ([]GetRunnersForDeploymentRow, error) InsertCallEvent(ctx context.Context, arg InsertCallEventParams) error InsertDeploymentCreatedEvent(ctx context.Context, arg InsertDeploymentCreatedEventParams) error diff --git a/backend/controller/sql/queries.sql b/backend/controller/sql/queries.sql index 0a72d5910d..b19db0a8be 100644 --- a/backend/controller/sql/queries.sql +++ b/backend/controller/sql/queries.sql @@ -122,7 +122,7 @@ WITH matches AS ( UPDATE runners SET state = 'dead', deployment_id = NULL - WHERE key = $1 + WHERE key = sqlc.arg('key')::runner_key RETURNING 1) SELECT COUNT(*) FROM matches; @@ -222,7 +222,7 @@ RETURNING runners.*; -- name: GetRunnerState :one SELECT state FROM runners -WHERE key = $1; +WHERE key = sqlc.arg('key')::runner_key; -- name: GetRunner :one SELECT DISTINCT ON (r.key) r.key AS runner_key, @@ -236,7 +236,7 @@ SELECT DISTINCT ON (r.key) r.key AS runner_key THEN d.name END, NULL) AS deployment_name FROM runners r LEFT JOIN deployments d on d.id = r.deployment_id OR r.deployment_id IS NULL -WHERE r.key = $1; +WHERE r.key = sqlc.arg('key')::runner_key; -- name: GetRoutingTable :many SELECT endpoint, r.key AS runner_key, r.module_name, d.name deployment_name @@ -251,7 +251,7 @@ WHERE state = 'assigned' SELECT endpoint, r.key AS runner_key, r.module_name, d.name deployment_name, r.state FROM runners r LEFT JOIN deployments d on r.deployment_id = d.id -WHERE r.key = $1; +WHERE r.key = sqlc.arg('key')::runner_key; -- name: GetRunnersForDeployment :many SELECT * diff --git a/backend/controller/sql/queries.sql.go b/backend/controller/sql/queries.sql.go index edb0b58296..073a0c3ba8 100644 --- a/backend/controller/sql/queries.sql.go +++ b/backend/controller/sql/queries.sql.go @@ -100,13 +100,13 @@ WITH matches AS ( UPDATE runners SET state = 'dead', deployment_id = NULL - WHERE key = $1 + WHERE key = $1::runner_key RETURNING 1) SELECT COUNT(*) FROM matches ` -func (q *Queries) DeregisterRunner(ctx context.Context, key Key) (int64, error) { +func (q *Queries) DeregisterRunner(ctx context.Context, key model.RunnerKey) (int64, error) { row := q.db.QueryRow(ctx, deregisterRunner, key) var count int64 err := row.Scan(&count) @@ -225,7 +225,7 @@ ORDER BY r.key ` type GetActiveRunnersRow struct { - RunnerKey Key + RunnerKey model.RunnerKey Endpoint string State RunnerState Labels []byte @@ -680,7 +680,7 @@ WHERE r.state = 'assigned' ` type GetIngressRoutesRow struct { - RunnerKey Key + RunnerKey model.RunnerKey DeploymentName model.DeploymentName Endpoint string Path string @@ -759,7 +759,7 @@ type GetProcessListRow struct { MinReplicas int32 DeploymentName model.DeploymentName DeploymentLabels []byte - RunnerKey NullKey + RunnerKey NullRunnerKey Endpoint optional.Option[string] RunnerLabels []byte } @@ -795,19 +795,19 @@ const getRouteForRunner = `-- name: GetRouteForRunner :one SELECT endpoint, r.key AS runner_key, r.module_name, d.name deployment_name, r.state FROM runners r LEFT JOIN deployments d on r.deployment_id = d.id -WHERE r.key = $1 +WHERE r.key = $1::runner_key ` type GetRouteForRunnerRow struct { Endpoint string - RunnerKey Key + RunnerKey model.RunnerKey ModuleName optional.Option[string] DeploymentName model.DeploymentName State RunnerState } // Retrieve routing information for a runner. -func (q *Queries) GetRouteForRunner(ctx context.Context, key Key) (GetRouteForRunnerRow, error) { +func (q *Queries) GetRouteForRunner(ctx context.Context, key model.RunnerKey) (GetRouteForRunnerRow, error) { row := q.db.QueryRow(ctx, getRouteForRunner, key) var i GetRouteForRunnerRow err := row.Scan( @@ -831,7 +831,7 @@ WHERE state = 'assigned' type GetRoutingTableRow struct { Endpoint string - RunnerKey Key + RunnerKey model.RunnerKey ModuleName optional.Option[string] DeploymentName model.DeploymentName } @@ -873,11 +873,11 @@ SELECT DISTINCT ON (r.key) r.key AS runner_key THEN d.name END, NULL) AS deployment_name FROM runners r LEFT JOIN deployments d on d.id = r.deployment_id OR r.deployment_id IS NULL -WHERE r.key = $1 +WHERE r.key = $1::runner_key ` type GetRunnerRow struct { - RunnerKey Key + RunnerKey model.RunnerKey Endpoint string State RunnerState Labels []byte @@ -886,7 +886,7 @@ type GetRunnerRow struct { DeploymentName optional.Option[string] } -func (q *Queries) GetRunner(ctx context.Context, key Key) (GetRunnerRow, error) { +func (q *Queries) GetRunner(ctx context.Context, key model.RunnerKey) (GetRunnerRow, error) { row := q.db.QueryRow(ctx, getRunner, key) var i GetRunnerRow err := row.Scan( @@ -904,10 +904,10 @@ func (q *Queries) GetRunner(ctx context.Context, key Key) (GetRunnerRow, error) const getRunnerState = `-- name: GetRunnerState :one SELECT state FROM runners -WHERE key = $1 +WHERE key = $1::runner_key ` -func (q *Queries) GetRunnerState(ctx context.Context, key Key) (RunnerState, error) { +func (q *Queries) GetRunnerState(ctx context.Context, key model.RunnerKey) (RunnerState, error) { row := q.db.QueryRow(ctx, getRunnerState, key) var state RunnerState err := row.Scan(&state) @@ -924,7 +924,7 @@ WHERE state = 'assigned' type GetRunnersForDeploymentRow struct { ID int64 - Key Key + Key model.RunnerKey Created time.Time LastSeen time.Time ReservationTimeout NullTime @@ -1339,7 +1339,7 @@ RETURNING deployment_id ` type UpsertRunnerParams struct { - Key Key + Key model.RunnerKey Endpoint string State RunnerState Labels []byte diff --git a/backend/controller/sql/schema/001_init.sql b/backend/controller/sql/schema/001_init.sql index 33905f2b93..695c39cd4f 100644 --- a/backend/controller/sql/schema/001_init.sql +++ b/backend/controller/sql/schema/001_init.sql @@ -37,6 +37,9 @@ CREATE TABLE modules -- Proto-encoded module schema. CREATE DOMAIN module_schema_pb AS BYTEA; +CREATE DOMAIN runner_key AS varchar; +CREATE DOMAIN controller_key AS varchar; + CREATE TABLE deployments ( id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -101,7 +104,7 @@ CREATE TABLE runners ( id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- Unique identifier for this runner, generated at startup. - key UUID UNIQUE NOT NULL, + key runner_key UNIQUE NOT NULL, created TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'), last_seen TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'), -- If the runner is reserved, this is the time at which the reservation expires. @@ -209,7 +212,7 @@ CREATE TYPE controller_state AS ENUM ( CREATE TABLE controller ( id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - key UUID UNIQUE NOT NULL, + key controller_key UNIQUE NOT NULL, created TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'), last_seen TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'), state controller_state NOT NULL DEFAULT 'live', diff --git a/backend/controller/sql/types.go b/backend/controller/sql/types.go index edd3627665..168b91843b 100644 --- a/backend/controller/sql/types.go +++ b/backend/controller/sql/types.go @@ -1,61 +1,18 @@ package sql import ( + "database/sql" "database/sql/driver" - "fmt" "time" "github.com/alecthomas/types/optional" - "github.com/google/uuid" - "github.com/oklog/ulid/v2" -) - -type NullKey = optional.Option[Key] - -// FromOption converts a optional.Option[~ulid.ULID] to a NullKey. -func FromOption[T ~[16]byte](o optional.Option[T]) NullKey { - if v, ok := o.Get(); ok { - return SomeKey(Key(v)) - } - return NoneKey() -} -func SomeKey(key Key) NullKey { return optional.Some(key) } -func NoneKey() NullKey { return optional.None[Key]() } - -// Key is a ULID that can be used as a column in a database. -type Key ulid.ULID - -func (u Key) Value() (driver.Value, error) { - bytes := u[:] - return bytes, nil -} -func (u *Key) Scan(src any) error { - switch src := src.(type) { - case string: - id, err := uuid.Parse(src) - if err != nil { - return err - } - *u = Key(id) - - case Key: - *u = src - - default: - return fmt.Errorf("invalid key type %T", src) - } - return nil -} - -func (u *Key) UnmarshalText(text []byte) error { - id, err := uuid.ParseBytes(text) - if err != nil { - return err - } - *u = Key(id) - return nil -} + "github.com/TBD54566975/ftl/internal/model" +) type NullTime = optional.Option[time.Time] type NullDuration = optional.Option[time.Duration] +type NullRunnerKey = optional.Option[model.RunnerKey] + +var _ sql.Scanner = (*NullRunnerKey)(nil) +var _ driver.Valuer = (*NullRunnerKey)(nil) diff --git a/backend/protos/xyz/block/ftl/v1/ftl.pb.go b/backend/protos/xyz/block/ftl/v1/ftl.pb.go index 6757b799b2..3f7c54da61 100644 --- a/backend/protos/xyz/block/ftl/v1/ftl.pb.go +++ b/backend/protos/xyz/block/ftl/v1/ftl.pb.go @@ -1254,7 +1254,6 @@ type RegisterRunnerRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // UUID representing the runner instance. Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"` Deployment *string `protobuf:"bytes,3,opt,name=deployment,proto3,oneof" json:"deployment,omitempty"` diff --git a/backend/protos/xyz/block/ftl/v1/ftl.proto b/backend/protos/xyz/block/ftl/v1/ftl.proto index 29d570ce5f..4f4f99c840 100644 --- a/backend/protos/xyz/block/ftl/v1/ftl.proto +++ b/backend/protos/xyz/block/ftl/v1/ftl.proto @@ -146,7 +146,6 @@ enum RunnerState { } message RegisterRunnerRequest { - // UUID representing the runner instance. string key = 1; string endpoint = 2; optional string deployment = 3; diff --git a/backend/runner/runner.go b/backend/runner/runner.go index d6a1dd04c3..a3ca551f2a 100644 --- a/backend/runner/runner.go +++ b/backend/runner/runner.go @@ -40,7 +40,7 @@ type Config struct { Config []string `name:"config" short:"C" help:"Paths to FTL project configuration files." env:"FTL_CONFIG" placeholder:"FILE[,FILE,...]" type:"existingfile"` Bind *url.URL `help:"Endpoint the Runner should bind to and advertise." default:"http://localhost:8893" env:"FTL_RUNNER_BIND"` Advertise *url.URL `help:"Endpoint the Runner should advertise (use --bind if omitted)." default:"" env:"FTL_RUNNER_ADVERTISE"` - Key model.RunnerKey `help:"Runner key (auto)." placeholder:"R" default:"R00000000000000000000000000"` + Key model.RunnerKey `help:"Runner key (auto)."` ControllerEndpoint *url.URL `name:"ftl-endpoint" help:"Controller endpoint." env:"FTL_ENDPOINT" default:"http://localhost:8892"` TemplateDir string `help:"Template directory to copy into each deployment, if any." type:"existingdir"` DeploymentDir string `help:"Directory to store deployments in." default:"${deploymentdir}"` @@ -76,7 +76,7 @@ func Start(ctx context.Context, config Config) error { key := config.Key if key == (model.RunnerKey{}) { - key = model.NewRunnerKey() + key = model.NewRunnerKey(config.Bind.Hostname(), config.Bind.Port()) } labels, err := structpb.NewStruct(map[string]any{ "hostname": hostname, diff --git a/buildengine/testdata/modules/alpha/go.mod b/buildengine/testdata/modules/alpha/go.mod index 0f29f19b74..45c43bb816 100644 --- a/buildengine/testdata/modules/alpha/go.mod +++ b/buildengine/testdata/modules/alpha/go.mod @@ -19,7 +19,6 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -27,7 +26,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/swaggest/jsonschema-go v0.3.69 // indirect github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.3 // indirect diff --git a/buildengine/testdata/modules/alpha/go.sum b/buildengine/testdata/modules/alpha/go.sum index 07b4b095c4..79f525dbf9 100644 --- a/buildengine/testdata/modules/alpha/go.sum +++ b/buildengine/testdata/modules/alpha/go.sum @@ -66,9 +66,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/buildengine/testdata/modules/another/go.mod b/buildengine/testdata/modules/another/go.mod index 26c343a84c..aeb27f8a25 100644 --- a/buildengine/testdata/modules/another/go.mod +++ b/buildengine/testdata/modules/another/go.mod @@ -19,7 +19,6 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -27,7 +26,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/swaggest/jsonschema-go v0.3.69 // indirect github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.3 // indirect diff --git a/buildengine/testdata/modules/another/go.sum b/buildengine/testdata/modules/another/go.sum index 07b4b095c4..79f525dbf9 100644 --- a/buildengine/testdata/modules/another/go.sum +++ b/buildengine/testdata/modules/another/go.sum @@ -66,9 +66,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/buildengine/testdata/modules/other/go.mod b/buildengine/testdata/modules/other/go.mod index ee87ff029e..e6347b399f 100644 --- a/buildengine/testdata/modules/other/go.mod +++ b/buildengine/testdata/modules/other/go.mod @@ -19,7 +19,6 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -27,7 +26,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/swaggest/jsonschema-go v0.3.69 // indirect github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.3 // indirect diff --git a/buildengine/testdata/modules/other/go.sum b/buildengine/testdata/modules/other/go.sum index 07b4b095c4..79f525dbf9 100644 --- a/buildengine/testdata/modules/other/go.sum +++ b/buildengine/testdata/modules/other/go.sum @@ -66,9 +66,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/cmd/ftl/cmd_serve.go b/cmd/ftl/cmd_serve.go index b12a7c2b10..be7bea3748 100644 --- a/cmd/ftl/cmd_serve.go +++ b/cmd/ftl/cmd_serve.go @@ -25,6 +25,7 @@ import ( "github.com/TBD54566975/ftl/internal/bind" "github.com/TBD54566975/ftl/internal/exec" "github.com/TBD54566975/ftl/internal/log" + "github.com/TBD54566975/ftl/internal/model" "github.com/TBD54566975/ftl/internal/rpc" "github.com/TBD54566975/ftl/internal/slices" ) @@ -101,6 +102,7 @@ func (s *serveCmd) Run(ctx context.Context) error { i := i config := controller.Config{ Bind: controllerAddresses[i], + Key: model.NewLocalControllerKey(i), DSN: dsn, AllowOrigins: s.AllowOrigins, NoConsole: s.NoConsole, diff --git a/examples/go/echo/go.mod b/examples/go/echo/go.mod index 9da318c7bb..149382bf94 100644 --- a/examples/go/echo/go.mod +++ b/examples/go/echo/go.mod @@ -21,7 +21,6 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -29,7 +28,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/swaggest/jsonschema-go v0.3.69 // indirect github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.3 // indirect diff --git a/examples/go/echo/go.sum b/examples/go/echo/go.sum index 07b4b095c4..79f525dbf9 100644 --- a/examples/go/echo/go.sum +++ b/examples/go/echo/go.sum @@ -66,9 +66,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/frontend/src/protos/xyz/block/ftl/v1/ftl_pb.ts b/frontend/src/protos/xyz/block/ftl/v1/ftl_pb.ts index e0024b4401..2b14de5627 100644 --- a/frontend/src/protos/xyz/block/ftl/v1/ftl_pb.ts +++ b/frontend/src/protos/xyz/block/ftl/v1/ftl_pb.ts @@ -1025,8 +1025,6 @@ export class GetDeploymentResponse extends Message { */ export class RegisterRunnerRequest extends Message { /** - * UUID representing the runner instance. - * * @generated from field: string key = 1; */ key = ""; diff --git a/go.mod b/go.mod index 6a94f0dde0..4cf08aa9af 100644 --- a/go.mod +++ b/go.mod @@ -23,14 +23,12 @@ require ( github.com/go-logr/logr v1.4.1 github.com/gofrs/flock v0.8.1 github.com/golang/protobuf v1.5.4 - github.com/google/uuid v1.6.0 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgx/v5 v5.5.5 github.com/jellydator/ttlcache/v3 v3.2.0 github.com/jpillora/backoff v1.0.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.20 - github.com/oklog/ulid/v2 v2.1.0 github.com/otiai10/copy v1.14.0 github.com/radovskyb/watcher v1.0.7 github.com/rs/cors v1.10.1 @@ -58,6 +56,7 @@ require ( ) require ( + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pkoukk/tiktoken-go v0.1.2 // indirect diff --git a/go.sum b/go.sum index e0048894fc..f78c9a93d3 100644 --- a/go.sum +++ b/go.sum @@ -129,13 +129,10 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkoukk/tiktoken-go v0.1.2 h1:u7PCSBiWJ3nJYoTGShyM9iHXz4dNyYkurwwp+GHtyHY= diff --git a/internal/model/keys.go b/internal/model/keys.go index 2449adb433..5740e7267f 100644 --- a/internal/model/keys.go +++ b/internal/model/keys.go @@ -2,85 +2,147 @@ package model import ( + "crypto/rand" "database/sql" "database/sql/driver" "fmt" "reflect" - "regexp" "strings" - - "github.com/google/uuid" - "github.com/oklog/ulid/v2" ) -func NewRunnerKey() RunnerKey { return RunnerKey(ulid.Make()) } -func ParseRunnerKey(key string) (RunnerKey, error) { return parseKey[RunnerKey](key) } +func NewRunnerKey(hostname string, port string) RunnerKey { + hash := make([]byte, 4) + _, err := rand.Read(hash) + if err != nil { + panic(err) + } + return keyType[runnerKey]{ + Hostname: hostname, + Port: port, + Suffix: fmt.Sprintf("%08x", hash), + } +} +func NewLocalRunnerKey(suffix int) RunnerKey { + return keyType[runnerKey]{ + Suffix: fmt.Sprintf("%04d", suffix), + } +} +func ParseRunnerKey(key string) (RunnerKey, error) { return parseKey[RunnerKey](key, true) } type runnerKey struct{} type RunnerKey = keyType[runnerKey] -func NewControllerKey() ControllerKey { return ControllerKey(ulid.Make()) } -func ParseControllerKey(key string) (ControllerKey, error) { return parseKey[ControllerKey](key) } +func NewControllerKey(hostname string, port string) ControllerKey { + hash := make([]byte, 4) + _, err := rand.Read(hash) + if err != nil { + panic(err) + } + return keyType[controllerKey]{ + Hostname: hostname, + Port: port, + Suffix: fmt.Sprintf("%08x", hash), + } +} + +func NewLocalControllerKey(suffix int) ControllerKey { + return keyType[controllerKey]{ + Suffix: fmt.Sprintf("%04d", suffix), + } +} +func ParseControllerKey(key string) (ControllerKey, error) { return parseKey[ControllerKey](key, true) } type controllerKey struct{} type ControllerKey = keyType[controllerKey] -var uuidRe = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) +func parseKey[KT keyType[U], U any](key string, includesKind bool) (KT, error) { + // Expected style: [-]-- or [-] -func parseKey[KT keyType[U], U any](key string) (KT, error) { - var zero KT - kind := kindFromType[U]() - switch { - case strings.HasPrefix(key, kind): - ulid, err := ulid.Parse(key[len(kind):]) - if err != nil { - return zero, fmt.Errorf("%s: %w", "invalid ULID key", err) + components := strings.Split(key, "-") + if includesKind { + // + if len(components) == 0 { + return KT{}, fmt.Errorf("expected a prefix for key: %s", key) } - return KT(ulid), nil - - case uuidRe.MatchString(key): - uuid, err := uuid.Parse(key) - if err != nil { - return zero, fmt.Errorf("%s: %w", "invalid UUID key", err) + kind := kindFromType[U]() + if components[0] != kind { + return KT{}, fmt.Errorf("unexpected prefix for key: %s", key) } - return KT(uuid), nil + components = components[1:] + } + switch { + case len(components) == 1: + //style: [-] + return KT{ + Suffix: components[0], + }, nil + case len(components) >= 3: + //style: [-]-- + suffix := components[len(components)-1] + port := components[len(components)-2] + host := strings.Join(components[:len(components)-2], "-") + + return KT{ + Hostname: host, + Port: port, + Suffix: suffix, + }, nil default: - return zero, fmt.Errorf("invalid %s key %q", kind, key) + return KT{}, fmt.Errorf("expected more components in key: %s", key) } + } // Helper type to avoid having to write a bunch of boilerplate. It relies on T being a // named struct in the form Key, eg. "runnerKey" -type keyType[T any] ulid.ULID +type keyType[T any] struct { + Hostname string + Port string + Suffix string +} func (d keyType[T]) Value() (driver.Value, error) { - return uuid.UUID(d), nil + return d.string(false), nil } var _ sql.Scanner = (*keyType[int])(nil) var _ driver.Valuer = (*keyType[int])(nil) -// Scan from UUID DB representation. +// Scan from DB representation. func (d *keyType[T]) Scan(src any) error { input, ok := src.(string) if !ok { - return fmt.Errorf("expected UUID to be a string but it's a %T", src) + return fmt.Errorf("expected key to be a string but it's a %T", src) } - id, err := uuid.Parse(input) + key, err := parseKey[keyType[T]](input, false) if err != nil { - return fmt.Errorf("%s: %w", "invalid UUID", err) + return err } - *d = keyType[T](id) + *d = key return nil } -func (d keyType[T]) Kind() string { return kindFromType[T]() } -func (d keyType[T]) String() string { return d.Kind() + ulid.ULID(d).String() } -func (d keyType[T]) ULID() ulid.ULID { return ulid.ULID(d) } +func (d keyType[T]) Kind() string { return kindFromType[T]() } + +func (d keyType[T]) String() string { + return d.string(true) +} + +func (d keyType[T]) string(includeKind bool) string { + var prefix string + if includeKind { + prefix = fmt.Sprintf("%s-", d.Kind()) + } + if d.Hostname == "" { + return fmt.Sprintf("%s%s", prefix, d.Suffix) + } + return fmt.Sprintf("%s%s-%s-%s", prefix, d.Hostname, d.Port, d.Suffix) +} + func (d keyType[T]) MarshalText() ([]byte, error) { return []byte(d.String()), nil } func (d *keyType[T]) UnmarshalText(bytes []byte) error { - id, err := parseKey[keyType[T]](string(bytes)) + id, err := parseKey[keyType[T]](string(bytes), true) if err != nil { return err } @@ -90,5 +152,5 @@ func (d *keyType[T]) UnmarshalText(bytes []byte) error { func kindFromType[T any]() string { var zero T - return strings.ToUpper(strings.TrimSuffix(reflect.TypeOf(zero).Name(), "Key")[:1]) + return strings.ToLower(strings.TrimSuffix(reflect.TypeOf(zero).Name(), "Key")[:1]) } diff --git a/internal/model/keys_test.go b/internal/model/keys_test.go index e8772e16a4..48e6158454 100644 --- a/internal/model/keys_test.go +++ b/internal/model/keys_test.go @@ -8,9 +8,50 @@ import ( ) func TestRunnerKey(t *testing.T) { - expected := NewRunnerKey() - assert.True(t, strings.HasPrefix(expected.String(), "R")) - actual, err := ParseRunnerKey(expected.String()) - assert.NoError(t, err) - assert.Equal(t, expected, actual) + for _, test := range []struct { + key RunnerKey + str string + strPrefix string + value string + valuePrefix string + }{ + // Production Keys + {key: NewRunnerKey("0.0.0.0", "8080"), strPrefix: "r-0.0.0.0-8080-", valuePrefix: "0.0.0.0-8080-"}, + {key: NewRunnerKey("example-host-with-hyphens", "0"), strPrefix: "r-example-host-with-hyphens-0-", valuePrefix: "example-host-with-hyphens-0-"}, + {key: NewRunnerKey("noport", ""), strPrefix: "r-noport--", valuePrefix: "noport--"}, + {key: NewRunnerKey("r-hostwithsameprefix", "80"), strPrefix: "r-r-hostwithsameprefix-80-", valuePrefix: "r-hostwithsameprefix-80-"}, + {key: NewRunnerKey("r-hostwithprefixandfakeport-80", "80"), strPrefix: "r-r-hostwithprefixandfakeport-80-80-", valuePrefix: "r-hostwithprefixandfakeport-80-80-"}, + + // Local Keys + {key: NewLocalRunnerKey(0), str: "r-0000", value: "0000"}, + {key: NewLocalRunnerKey(1), str: "r-0001", value: "0001"}, + {key: NewLocalRunnerKey(9999), str: "r-9999", value: "9999"}, + {key: NewLocalRunnerKey(12345), str: "r-12345", value: "12345"}, + } { + if test.str != "" { + assert.Equal(t, test.str, test.key.String(), "expected string %q for %q", test.str, test.key.String()) + } + if test.strPrefix != "" { + assert.True(t, strings.HasPrefix(test.key.String(), test.strPrefix), "expected string prefix %q for %q", test.strPrefix, test.key.String()) + } + aValue, err := test.key.Value() + assert.NoError(t, err) + value, ok := aValue.(string) + assert.True(t, ok, "expected string value for %v", aValue) + + if test.value != "" { + assert.Equal(t, test.value, value, "expected value %q for %q", test.value, value) + } + if test.valuePrefix != "" { + assert.True(t, strings.HasPrefix(value, test.valuePrefix), "expected value prefix %q for %q", test.valuePrefix, value) + } + + parsed, err := ParseRunnerKey(test.key.String()) + assert.NoError(t, err) + assert.Equal(t, test.key, parsed, "expected %v for %v after parsing", test.key, parsed) + + parsed, err = parseKey[RunnerKey](value, false) + assert.NoError(t, err) + assert.Equal(t, test.key, parsed, "expected %v for %v after parsing db key", test.key, parsed) + } } diff --git a/sqlc.yaml b/sqlc.yaml index eaa0c51e59..3211426d7b 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -27,16 +27,17 @@ sql: nullable: true go_type: type: "NullTime" - - db_type: "uuid" - go_type: - type: "Key" - - db_type: "uuid" - nullable: true - go_type: - type: "NullKey" - db_type: "pg_catalog.varchar" nullable: true go_type: "github.com/alecthomas/types/optional.Option[string]" + - db_type: "runner_key" + go_type: "github.com/TBD54566975/ftl/internal/model.RunnerKey" + - db_type: "runner_key" + nullable: true + go_type: + type: "NullRunnerKey" + - db_type: "controller_key" + go_type: "github.com/TBD54566975/ftl/internal/model.ControllerKey" - db_type: "text" go_type: "string" - db_type: "text"