Problem
Lifecycle type (buildpack, docker, cnb) is not stored directly on apps, droplets, or builds. It is derived at runtime by checking for the existence of associated lifecycle data records:
# Current: checks multiple associations
def lifecycle_type
return 'buildpack' if buildpack_lifecycle_data
return 'cnb' if cnb_lifecycle_data
'docker'
end
This causes:
- Expensive list queries —
GET /v3/apps?lifecycle_type=docker requires excluding apps from both buildpack_lifecycle_data and cnb_lifecycle_data tables via subqueries
- Unnecessary eager loading — Presenters load lifecycle data associations just to determine the type string, even when the full lifecycle data is not needed
- Implicit semantics — "docker" is the absence of other types, not an explicit value
Proposed Solution
Add a lifecycle_type VARCHAR column (with index) to apps, droplets, and builds tables.
Deployment Strategy (Two Phases, Zero Downtime)
Deploy 1 (draft PR: #5065):
- Add nullable columns with concurrent index
- Populate for new records via
AppCreate, V2::AppCreate, and before_create hooks on droplets/builds
- Fallback to association lookup for existing records
- Model validation ensures only valid lifecycle types (buildpack, cnb, docker)
Deploy 2:
- Backfill existing records based on lifecycle data tables
- Make columns non-nullable
- Remove fallback logic
- Optimize
AppListFetcher: subqueries → simple WHERE clause
- Optimize presenters: skip eager-loading lifecycle data for type determination
What the Draft PR Demonstrates
- Migration adds column + concurrent index (Postgres) / regular index (MySQL)
AppCreate / V2::AppCreate set lifecycle_type explicitly
DropletModel / BuildModel inherit via before_create hook from associated app
lifecycle_type method returns column value with fallback for un-migrated records
lifecycle_data method uses lifecycle_type to select the right association (avoids checking all associations)
- Model validation ensures only valid lifecycle types
DropletCopy validates lifecycle type match between source and destination
- Lifecycle classes assign created lifecycle data back to model instance to avoid Sequel caching issues
Impact After Deploy 2
| Area |
Before |
After |
AppListFetcher lifecycle_type filter |
3 subqueries on lifecycle data tables |
1 simple WHERE lifecycle_type = ? |
| Presenters (App, Build, Droplet, Process) |
Eager-load lifecycle data associations to determine type |
Read column value directly |
| Lifecycle type checks in controllers |
Association lookups |
O(1) column read |
| Docker type semantics |
Implicit (absence of other types) |
Explicit column value |
Endpoints That Benefit
Critical: GET /v3/apps with lifecycle_type filter (subquery elimination)
High: All list and detail endpoints for apps, droplets, builds, and processes (presenter eager-load avoidance)
Medium: POST /v3/apps, POST /v3/builds, POST /v3/apps/:guid/actions/start, POST /v3/apps/:guid/actions/restage, POST /v3/spaces/:guid/actions/apply_manifest, POST /v3/droplets/:guid/copy, staging completion (controller-level type checks)
Problem
Lifecycle type (buildpack, docker, cnb) is not stored directly on apps, droplets, or builds. It is derived at runtime by checking for the existence of associated lifecycle data records:
This causes:
GET /v3/apps?lifecycle_type=dockerrequires excluding apps from bothbuildpack_lifecycle_dataandcnb_lifecycle_datatables via subqueriesProposed Solution
Add a
lifecycle_typeVARCHAR column (with index) toapps,droplets, andbuildstables.Deployment Strategy (Two Phases, Zero Downtime)
Deploy 1 (draft PR: #5065):
AppCreate,V2::AppCreate, andbefore_createhooks on droplets/buildsDeploy 2:
AppListFetcher: subqueries → simpleWHEREclauseWhat the Draft PR Demonstrates
AppCreate/V2::AppCreatesetlifecycle_typeexplicitlyDropletModel/BuildModelinherit viabefore_createhook from associated applifecycle_typemethod returns column value with fallback for un-migrated recordslifecycle_datamethod useslifecycle_typeto select the right association (avoids checking all associations)DropletCopyvalidates lifecycle type match between source and destinationImpact After Deploy 2
AppListFetcherlifecycle_type filterWHERE lifecycle_type = ?Endpoints That Benefit
Critical:
GET /v3/appswithlifecycle_typefilter (subquery elimination)High: All list and detail endpoints for apps, droplets, builds, and processes (presenter eager-load avoidance)
Medium:
POST /v3/apps,POST /v3/builds,POST /v3/apps/:guid/actions/start,POST /v3/apps/:guid/actions/restage,POST /v3/spaces/:guid/actions/apply_manifest,POST /v3/droplets/:guid/copy, staging completion (controller-level type checks)