---
# System prepended metadata

title: Queue Overload - Autoscaling

---

# Queue Overload - Autoscaling

## Navigation

1. [Problem](https://hackmd.io/@jwdunne/HJXyhNY4h)
2. [Observability](https://hackmd.io/@jwdunne/S1pJ1CgHn)
3. [Testing](https://hackmd.io/@jwdunne/H1zKkAeSn)
4. [Throughput optimisation opportunities](https://hackmd.io/@jwdunne/H1h2k0xH3)
5. [Backpressure](https://hackmd.io/@jwdunne/B1WZeCeBh)
6. [Load shedding](https://hackmd.io/@jwdunne/BJB4MReH2)
7. [Autoscaling](https://hackmd.io/@jwdunne/Bkw_zAxHn)

## Solution

> This depends on a working solution for DB connection pooling for it to be fully effective. We can, however, get this working to within the same capacity as we have now. 

We can configure ECS service to auto-scale based on a custom metric and a target value. AWS allows us to:

1. Set a min and max capacity, to prevent runaway costs
2. Set a scaling up cooldown period, so it doesn't create a runaway scaling up effect
3. Set a scaling down cooldown period, so it doesn't scale down too early, allowing the problem to come back

We should be conservative with these values to begin with and iterate.

This is also the general approach to autoscaling ECS services. If we need to do this for API workers, this is how we would do it. 

#### Variables

To control autoscaling across environments (it'd be undesirable in staging long-term but useful to test), we should define a set of variables to control autoscaling behaviour. 

```terraform
variable "autoscaling_worker" {
	description = "Controls autoscaling parameters for workers"
	type = object({
		min_capacity = number
		max_capacity = number
		target = number
		scale_in_cooldown_seconds = number
		scale_out_cooldown_seconds = number
	})
}
```

#### Set up a custom CloudWatch metric

We just need to send a metric to CloudWatch from the application:

```php
$cloudWatch->putMetricData([
	'MetricData' => [
		[
			'MetricName' => 'JobQueuedCompletedRatio'
			'Value' => $queueLoad->ratio()
		]
	],
	'Namespace' => 'Leadflo/Queue'
])
```

We should sample this for every 5% (configurable) jobs queued to minimise the costs in sending the data to AWS whilst still giving reasonable data to AWS.

We can do this in a listener that listens for both `JobQueued` and `JobCompleted` events. This would be done using synchronous listeners since we cannot rely on the queue. This may incur some latency but we can dispatch events after the response is sent to the browser using FPM which should mitigate.

#### Set up a scalable target

This configures a target for application autoscaling. 

```terraform
resource "aws_appautoscaling_target" "worker" {
  max_capacity       = var.autoscaling_worker.min_capacity
  min_capacity       = var.autoscaling_worker.max_capacity
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.leadflo_worker.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}
```

The variables allow us to keep autoscaling a flat 1 on staging environments.

#### Set up an auto-scaling policy

```terraform
resource "aws_appautoscaling_policy" "worker" {
  name               = "worker"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.worker.resource_id
  scalable_dimension = aws_appautoscaling_target.worker.scalable_dimension
  service_namespace  = aws_appautoscaling_target.worker.service_namespace

  target_tracking_scaling_policy_configuration {
    target_value = var.autoscaling_worker.target
    scale_in_cooldown = var.autoscaling_worker.scale_in_cooldown_seconds
    scale_out_cooldown = var.autoscaling_worker.scale_out_cooldown_seconds

	customized_metric_specification {
		namespace = "Leadflo/Queue"
		metric_name = "JobQueuedCompletedRatio"
		statistic = "Average"
		unit = "Percent"
	}
  }
}
```

## Implementation

- Implement an IAM role for sending metrics to CloudWatch
- Implement a Terraform variable for configuring worker autoscaling
- Configure a scalable target and auto-scaling policy