Skip to content

Commit

Permalink
fix: remove pg_notify usage in favor of polling (#2094)
Browse files Browse the repository at this point in the history
Fixes #2087
  • Loading branch information
wesbillman authored Jul 17, 2024
1 parent 894b473 commit 802aa90
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 9 deletions.
16 changes: 11 additions & 5 deletions backend/controller/controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controller

import (
"bytes"
"context"
sha "crypto/sha256"
"encoding/binary"
Expand Down Expand Up @@ -1516,7 +1517,7 @@ func (s *Service) updateControllersList(ctx context.Context) (time.Duration, err
func (s *Service) watchModuleChanges(ctx context.Context, sendChange func(response *ftlv1.PullSchemaResponse) error) error {
logger := log.FromContext(ctx)
type moduleStateEntry struct {
hash sha256.SHA256
hash []byte
minReplicas int
}
moduleState := map[string]moduleStateEntry{}
Expand Down Expand Up @@ -1551,6 +1552,8 @@ func (s *Service) watchModuleChanges(ctx context.Context, sendChange func(respon
s.dal.DeploymentChanges.Subscribe(deploymentChanges)
defer s.dal.DeploymentChanges.Unsubscribe(deploymentChanges)

go s.dal.PollDeployments(ctx)

for {
select {
case <-ctx.Done():
Expand All @@ -1575,16 +1578,19 @@ func (s *Service) watchModuleChanges(ctx context.Context, sendChange func(respon
CreateTime: timestamppb.New(message.CreatedAt),
MinReplicas: int32(message.MinReplicas),
}
moduleSchemaBytes, err := proto.Marshal(moduleSchema)
if err != nil {

hasher := sha.New()
data := []byte(moduleSchema.String())
if _, err := hasher.Write(data); err != nil {
return err
}

newState := moduleStateEntry{
hash: sha256.FromBytes(moduleSchemaBytes),
hash: hasher.Sum(nil),
minReplicas: message.MinReplicas,
}
if current, ok := moduleState[message.Schema.Name]; ok {
if current != newState {
if !bytes.Equal(current.hash, newState.hash) || current.minReplicas != newState.minReplicas {
changeType := ftlv1.DeploymentChangeType_DEPLOYMENT_CHANGED
// A deployment is considered removed if its minReplicas is set to 0.
if current.minReplicas > 0 && message.MinReplicas == 0 {
Expand Down
7 changes: 3 additions & 4 deletions backend/controller/dal/dal.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,15 @@ func WithReservation(ctx context.Context, reservation Reservation, fn func() err
}

func New(ctx context.Context, pool *pgxpool.Pool) (*DAL, error) {
conn, err := pool.Acquire(ctx)
_, err := pool.Acquire(ctx)
if err != nil {
return nil, fmt.Errorf("failed to acquire PG PubSub connection: %w", err)
return nil, fmt.Errorf("could not acquire connection: %w", err)
}
dal := &DAL{
db: sql.NewDB(pool),
DeploymentChanges: pubsub.New[DeploymentNotification](),
}
go dal.runListener(ctx, conn.Hijack())

return dal, nil
}

Expand All @@ -233,7 +233,6 @@ type DAL struct {

// DeploymentChanges is a Topic that receives changes to the deployments table.
DeploymentChanges *pubsub.Topic[DeploymentNotification]
// RouteChanges is a Topic that receives changes to the routing table.
}

// Tx is DAL within a transaction.
Expand Down
1 change: 1 addition & 0 deletions backend/controller/dal/dal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ func TestDAL(t *testing.T) {
})

t.Run("VerifyDeploymentNotifications", func(t *testing.T) {
t.Skip("Skipping this test since we're not using the deployment notification system")
dal.DeploymentChanges.Unsubscribe(deploymentChangesCh)
expectedDeploymentChanges := []DeploymentNotification{
{Message: optional.Some(Deployment{Language: "go", Module: "test", Schema: &schema.Module{Name: "test"}})},
Expand Down
84 changes: 84 additions & 0 deletions backend/controller/dal/notify.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dal

import (
"bytes"
"context"
"crypto/sha256"
"encoding"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -51,6 +53,88 @@ type event struct {
Deleted json.RawMessage `json:"deleted,omitempty"`
}

type deploymentState struct {
Key model.DeploymentKey
schemaHash []byte
minReplicas int
}

func deploymentStateFromDeployment(deployment Deployment) (deploymentState, error) {
hasher := sha256.New()
data := []byte(deployment.Schema.String())
if _, err := hasher.Write(data); err != nil {
return deploymentState{}, fmt.Errorf("failed to hash schema: %w", err)
}

return deploymentState{
schemaHash: hasher.Sum(nil),
minReplicas: deployment.MinReplicas,
}, nil
}

func (d *DAL) PollDeployments(ctx context.Context) {
logger := log.FromContext(ctx)
retry := backoff.Backoff{}

previousDeployments := make(map[string]deploymentState)

for {
delay := time.Millisecond * 500
currentDeployments := make(map[string]deploymentState)

deployments, err := d.GetDeploymentsWithMinReplicas(ctx)
if err != nil {
logger.Errorf(err, "failed to get deployments")
time.Sleep(retry.Duration())
continue
}

// Check for new or updated deployments
for _, deployment := range deployments {
name := deployment.Schema.Name
state, err := deploymentStateFromDeployment(deployment)
if err != nil {
logger.Errorf(err, "failed to compute deployment state")
continue
}

currentDeployments[name] = state

previousState, exists := previousDeployments[name]
if !exists {
logger.Tracef("New deployment: %s", name)
d.DeploymentChanges.Publish(DeploymentNotification{
Message: optional.Some(deployment),
})
} else if !bytes.Equal(previousState.schemaHash, state.schemaHash) || previousState.minReplicas != state.minReplicas {
logger.Tracef("Changed deployment: %s", name)
d.DeploymentChanges.Publish(DeploymentNotification{
Message: optional.Some(deployment),
})
}
}

// Check for removed deployments
for name := range previousDeployments {
if _, exists := currentDeployments[name]; !exists {
logger.Tracef("Removed deployment: %s", name)
d.DeploymentChanges.Publish(DeploymentNotification{
Deleted: optional.Some(previousDeployments[name].Key),
})
}
}

previousDeployments = currentDeployments
retry.Reset()

select {
case <-ctx.Done():
return
case <-time.After(delay):
}
}
}

func (d *DAL) runListener(ctx context.Context, conn *pgx.Conn) {
defer conn.Close(ctx)
logger := log.FromContext(ctx)
Expand Down

0 comments on commit 802aa90

Please sign in to comment.