aws/hhvm1/state-machine/generate.hack (530 lines of code) (raw):

#!/usr/bin/env hhvm /* * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ /** * This script generates the Amazon States Language (a.k.a. JSON) code for the * state machine that builds and publishes HHVM release(s). * * The state machine has a lot of nesting, so it would be pretty hard to avoid * mistakes while trying to write the whole JSON by hand. There are also some * repeated patterns (e.g. retry policies) that are much easier to keep in sync * if we generate them. */ namespace Facebook\HHVMPackaging\GenerateStateMachine; use namespace HH\Lib\{C, Dict, Keyset, Str, Vec}; // Note: The enums below are not meant to be complete or accurate lists of // anything, they're just here to help avoid typos and inconsistencies in the // rest of the code. // Activities enum A: string as string { MakeSourceTarball = 'MakeSourceTarball'; MakeBinaryPackage = 'MakeBinaryPackage'; PublishBinaryPackages = 'PublishBinaryPackages'; PublishSourceTarball = 'PublishSourceTarball'; PublishDockerImages = 'PublishDockerImages'; BuildAndPublishMacOS = 'BuildAndPublishMacOS'; } // State names/name parts enum S: string as string { // Lambda states ParseInput = 'ParseInput'; GetPlatformsForVersion = 'GetPlatformsForVersion'; UpdateIndices = 'UpdateIndices'; InvalidateCloudFront = 'InvalidateCloudFront'; CheckIfReposChanged = 'CheckIfReposChanged'; NormalizeResults = 'NormalizeResults'; CheckForFailures = 'CheckForFailures'; HealthCheck = 'HealthCheck'; // Activity states' name prefixes PrepareTo = 'PrepareTo'; Should = 'Should'; // "Map" state name parts ForEach = 'ForEach'; Version = 'Version'; Platform = 'Platform'; // "Parallel" state name part And = 'And'; BuildAndPublishLinux = 'BuildAndPublishLinux'; // Suffix for an explicit "end" state, which we need to add to all nested // state machines because not all state types support {"End": true} End = 'End'; } // ASL state types enum T: string as string { Choice = 'Choice'; Map = 'Map'; Parallel = 'Parallel'; Pass = 'Pass'; Task = 'Task'; Wait = 'Wait'; } // ASL state fields enum F: string as string { Branches = 'Branches'; Catch = 'Catch'; Choices = 'Choices'; Default = 'Default'; End = 'End'; InputPath = 'InputPath'; ItemsPath = 'ItemsPath'; Iterator = 'Iterator'; Next = 'Next'; OutputPath = 'OutputPath'; Parameters = 'Parameters'; Resource = 'Resource'; ResultPath = 'ResultPath'; Retry = 'Retry'; Seconds = 'Seconds'; TimeoutSeconds = 'TimeoutSeconds'; Type = 'Type'; } // Common input/output parameter names enum P: string as string { BuildInput = 'buildInput'; Results = 'results'; Version = 'version'; Platform = 'platform'; Activity = 'activity'; } newtype State = dict<F, mixed>; newtype StateMachine = shape( 'StartAt' => string, 'States' => dict<string, State>, ); abstract final class Config { const dict<string, string> ARN = dict[ // Lambdas S::ParseInput => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-parse-input', S::GetPlatformsForVersion => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-get-platforms-for-version', S::UpdateIndices => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'create-s3-index-html', S::InvalidateCloudFront => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm-invalidate-repository-metadata-on-cloudfront', S::CheckIfReposChanged => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-check-if-repos-changed', S::NormalizeResults => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-normalize-results', S::CheckForFailures => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-check-for-failures', S::HealthCheck => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-health-check', S::PrepareTo => 'arn:aws:lambda:us-west-2:223121549624:function:'. 'hhvm1-prepare-activity', // Activities A::MakeSourceTarball => 'arn:aws:states:us-west-2:223121549624:activity:'. 'hhvm-make-source-tarball', A::MakeBinaryPackage => 'arn:aws:states:us-west-2:223121549624:activity:'. 'hhvm-make-binary-package', A::PublishBinaryPackages => 'arn:aws:states:us-west-2:223121549624:activity:'. 'hhvm-publish-binary-packages', A::PublishSourceTarball => 'arn:aws:states:us-west-2:223121549624:activity:'. 'hhvm-publish-source-tarball', A::PublishDockerImages => 'arn:aws:states:us-west-2:223121549624:activity:'. 'hhvm-publish-docker-images', A::BuildAndPublishMacOS => 'arn:aws:states:us-west-2:223121549624:activity:'. 'hhvm-build-and-publish-macos', ]; const dict<A, int> TIMEOUT_SEC = dict[ A::MakeSourceTarball => 30 * 60, A::MakeBinaryPackage => 4 * 60 * 60, A::PublishBinaryPackages => 240 * 60, A::PublishDockerImages => 240 * 60, A::PublishSourceTarball => 30 * 60, A::BuildAndPublishMacOS => 240 * 60, ]; /** * These launch EC2 instances or do other API calls, which tend to * intermittently fail (especially when running many at once). Hence, a lot of * retries. Note: AWS does 2x exponential backoff by default. */ const vec<dict<string, mixed>> LAMBDA_RETRY_POLICY = vec[ dict[ 'ErrorEquals' => vec['States.ALL'], // retry after 5, 10, 20, 40, 80 seconds 'IntervalSeconds' => 5, 'MaxAttempts' => 5, ], ]; /** * These do the actual building/publishing. This is expensive, and can * reasonably fail for legitimate reasons, so don't waste time with too * many retries. */ const vec<dict<string, mixed>> ACTIVITY_RETRY_POLICY = vec[ dict[ 'ErrorEquals' => vec['TestBuildNoRetry'], 'MaxAttempts' => 0, ], dict[ 'ErrorEquals' => vec['States.ALL'], // retry after 1 minute, then 5 minutes, then fail 'IntervalSeconds' => 60, 'MaxAttempts' => 2, 'BackoffRate' => 5.0, ], ]; } /** * Helper function to output JSON paths. */ function path(string ...$keys): string { return '$.'.Str\join($keys, '.'); } /** * Returns the value to use for F::Parameters that preserves the specified * parameters' values... */ function params(P ...$keys): dict<string, string> { return Dict\pull($keys, $key ==> path($key), $key ==> "$key.$"); } /** * ...and adds the specified parameter with the specified value. */ function params_with( keyset<P> $params_to_preserve, string $new_name, string $new_value, ): dict<string, string> { $params = params(...$params_to_preserve); $params[$new_name] = $new_value; return $params; } /** * ...and adds the specified parameter with value automatically produced by a * map state. */ function map_params( keyset<P> $params_to_preserve, P $map_param, ): dict<string, string> { return params_with($params_to_preserve, "$map_param.$", '$$.Map.Item.Value'); } /** * Returns the states needed for the common pattern: * PrepareTo$activity -> Should$activity -> $activity -> $next * `-----------------------------------^ */ function states_for_activity( keyset<P> $params_to_preserve, A $activity, string $next, string $failure_state, ): dict<string, State> { $params_to_preserve[] = P::BuildInput; return dict[ S::PrepareTo.$activity => dict[ F::Type => T::Task, F::Resource => Config::ARN[S::PrepareTo], F::Parameters => params_with($params_to_preserve, P::Activity, $activity), F::ResultPath => path(P::Results, $activity), F::Catch => vec[ dict[ 'ErrorEquals' => vec['States.ALL'], 'ResultPath' => path(P::Results, $activity, 'failure'), 'Next' => $failure_state, ], ], ], S::Should.$activity => dict[ F::Type => T::Choice, F::Choices => vec[ dict[ 'Variable' => path(P::Results, $activity, 'skip'), 'BooleanEquals' => true, 'Next' => $next, ], ], F::Default => $activity, ], $activity => dict[ F::Type => T::Task, F::Resource => Config::ARN[$activity], F::InputPath => path(P::Results, $activity, 'taskInput'), F::TimeoutSeconds => Config::TIMEOUT_SEC[$activity], F::Retry => Config::ACTIVITY_RETRY_POLICY, F::Next => $next, ], ]; } /** * Automatically adds StartAt and Next/End, assuming a linear state machine with * states in the provided order. Also adds standard values for most fields in * "Task" states if missing. Existing values are never overwritten. */ function linear_state_machine( ?string $failure_state, dict<string, State> $states, ): StateMachine { $names = Vec\keys($states); foreach ($names as $i => $name) { $type = $states[$name][F::Type]; if ($type === T::Task) { $states[$name][F::Resource] ??= Config::ARN[$name]; $states[$name][F::ResultPath] ??= path(P::Results, $name, 'success'); $states[$name][F::Retry] ??= Config::LAMBDA_RETRY_POLICY; if ($failure_state is nonnull) { $states[$name][F::Catch] ??= vec[ dict[ 'ErrorEquals' => vec['States.ALL'], 'ResultPath' => path(P::Results, $name, 'failure'), 'Next' => $failure_state, ], ]; } if (idx($states[$name], F::Catch) === vec[]) { unset($states[$name][F::Catch]); } } if ($type === T::Map || $type === T::Parallel) { $states[$name][F::ResultPath] ??= path(P::Results, $name); } if ( $type !== T::Choice && !C\contains_key($states[$name], F::Next) && !C\contains_key($states[$name], F::End) ) { $next = $names[$i+1] ?? null; if ($next is nonnull) { $states[$name][F::Next] = $next; } else { $states[$name][F::End] = true; } } } return shape( 'StartAt' => C\first_keyx($states), 'States' => $states, ); } /** * Adds in the Succes/Failure states that we need in every nested state machine. */ function nested_state_machine( string $end_state_prefix, ?P $add_param_to_results, dict<string, State> $states, ): StateMachine { return linear_state_machine( $end_state_prefix.S::End, Dict\merge( $states, dict[ $end_state_prefix.S::End => dict[ F::Type => T::Pass, F::Parameters => $add_param_to_results is nonnull ? params($add_param_to_results, P::Results) : params(P::Results), F::End => true, ], ], ), ); } /** * The two main parallel build branches. */ function linux_branch(): StateMachine { return nested_state_machine( S::BuildAndPublishLinux, null, Dict\merge( dict[ S::GetPlatformsForVersion => dict[ F::Type => T::Task, F::ResultPath => path('platforms'), ], S::ForEach.S::Platform => dict[ F::Type => T::Map, F::ItemsPath => path('platforms'), F::Parameters => map_params(keyset[P::BuildInput, P::Version], P::Platform), F::Iterator => nested_state_machine( S::Platform, P::Platform, Dict\merge( states_for_activity( keyset[P::Version, P::Platform], A::MakeBinaryPackage, S::Platform.S::End, S::Platform.S::End, ), ), ), ], ], states_for_activity( keyset[P::BuildInput, P::Version, P::Results], A::PublishBinaryPackages, A::PublishSourceTarball.S::And.A::PublishDockerImages, S::BuildAndPublishLinux.S::End, ), dict[ A::PublishSourceTarball.S::And.A::PublishDockerImages => dict[ F::Type => T::Parallel, F::Parameters => params(P::BuildInput, P::Version), F::Branches => vec[ nested_state_machine( A::PublishSourceTarball, null, states_for_activity( keyset[P::Version], A::PublishSourceTarball, A::PublishSourceTarball.S::End, A::PublishSourceTarball.S::End, ), ), nested_state_machine( A::PublishDockerImages, null, states_for_activity( keyset[P::Version], A::PublishDockerImages, A::PublishDockerImages.S::End, A::PublishDockerImages.S::End, ), ), ], ], ], ), ); } function macos_branch(): StateMachine { return nested_state_machine( A::BuildAndPublishMacOS, null, states_for_activity( keyset[P::Version], A::BuildAndPublishMacOS, A::BuildAndPublishMacOS.S::End, A::BuildAndPublishMacOS.S::End, ), ); } /** * The complete state machine is this main branch + a small branch that does * periodic health checks. */ function main_branch(): StateMachine { return linear_state_machine( null, dict[ S::ForEach.S::Version => dict[ F::Type => T::Map, F::ItemsPath => path(P::BuildInput, 'versions'), F::Parameters => map_params(keyset[P::BuildInput], P::Version), F::Iterator => linear_state_machine( S::Version.S::NormalizeResults, Dict\merge( states_for_activity( keyset[P::Version], A::MakeSourceTarball, S::BuildAndPublishLinux.S::And.A::BuildAndPublishMacOS, S::Version.S::NormalizeResults, ), dict[ S::BuildAndPublishLinux.S::And.A::BuildAndPublishMacOS => dict[ F::Type => T::Parallel, F::Parameters => params(P::BuildInput, P::Version), F::Branches => vec[linux_branch(), macos_branch()], ], S::Version.S::NormalizeResults => dict[ F::Type => T::Task, F::Resource => Config::ARN[S::NormalizeResults], F::Parameters => params(P::Version, P::Results), F::ResultPath => path('results'), F::Catch => vec[], F::End => true, ], ], ), ), ], S::CheckIfReposChanged => dict[ F::Type => T::Task, F::ResultPath => path('reposChanged'), ], S::Should.S::UpdateIndices.S::And.S::InvalidateCloudFront => dict[ F::Type => T::Choice, F::Choices => vec[ dict[ 'Variable' => path('reposChanged'), 'BooleanEquals' => false, 'Next' => S::NormalizeResults, ], ], F::Default => S::UpdateIndices.S::And.S::InvalidateCloudFront, ], S::UpdateIndices.S::And.S::InvalidateCloudFront => dict[ F::Type => T::Parallel, F::Parameters => params(P::BuildInput), F::Branches => vec[ nested_state_machine( S::UpdateIndices, null, dict[ S::UpdateIndices => dict[ F::Type => T::Task, F::Parameters => dict[ 'bucket' => 'hhvm-downloads', 'cloudfront' => 'E35YBTV6QCR5BA', ], ], ], ), nested_state_machine( S::InvalidateCloudFront, null, dict[ S::InvalidateCloudFront => dict[ F::Type => T::Task, ], ], ), ], ], S::NormalizeResults => dict[ F::Type => T::Task, F::ResultPath => '$', ], S::CheckForFailures => dict[ F::Type => T::Task, F::ResultPath => '$', // only retry on lambda service exceptions F::Retry => vec[ dict[ 'ErrorEquals' => vec[ 'Lambda.ServiceException', 'Lambda.AWSLambdaException', 'Lambda.SdkClientException', ], 'IntervalSeconds' => 5, 'MaxAttempts' => 5, ], ], ], ], ); } function generate(): StateMachine { return linear_state_machine( null, dict[ S::ParseInput => dict[ F::Type => T::Task, F::ResultPath => '$', ], 'Root' => dict[ F::Type => T::Parallel, F::OutputPath => path('results', 'Root[0]'), F::Branches => vec[ main_branch(), linear_state_machine( S::HealthCheck.S::End, dict[ S::HealthCheck.T::Wait => dict[ F::Type => T::Wait, F::Seconds => 300, ], S::HealthCheck => dict[ F::Type => T::Task, F::Parameters => dict[ 'buildInput.$' => '$.buildInput', 'execution.$' => '$$.Execution.Id', 'endState' => S::CheckIfReposChanged, ], F::ResultPath => path('shouldRepeat'), ], S::Should.S::HealthCheck => dict[ F::Type => T::Choice, F::Choices => vec[ dict[ 'Variable' => path('shouldRepeat'), 'BooleanEquals' => true, 'Next' => S::HealthCheck.T::Wait, ], ], F::Default => S::HealthCheck.S::End, ], S::HealthCheck.S::End => dict[ F::Type => T::Pass, F::End => true, ], ], ), ], ], ], ); } <<__EntryPoint>> function main(): void { require_once \dirname(__FILE__).'/vendor/autoload.hack'; \Facebook\AutoloadMap\initialize(); echo \json_encode(generate(), \JSON_PRETTY_PRINT)."\n"; }