├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── elixir_runtime ├── .formatter.exs ├── .gitignore ├── LICENSE.md ├── README.md ├── config │ └── config.exs ├── lib │ ├── elixir_runtime │ │ ├── application.ex │ │ ├── lambda_service_client.ex │ │ ├── loop.ex │ │ ├── loop │ │ │ ├── client.ex │ │ │ ├── handler.ex │ │ │ └── monitor.ex │ │ ├── monitor.ex │ │ └── monitor │ │ │ ├── client.ex │ │ │ ├── error.ex │ │ │ ├── server.ex │ │ │ └── state.ex │ ├── epmd │ │ └── stub_client.ex │ └── mix │ │ └── tasks │ │ ├── bootstrap.ex │ │ ├── gen_lambda_release.ex │ │ └── zip.ex ├── mix.exs ├── priv │ ├── libcrypto.so.1.0.0 │ └── libssl.so └── test │ ├── elixir_runtime │ ├── application_test.exs │ ├── loop │ │ └── handler_test.exs │ └── monitor │ │ ├── error_test.exs │ │ └── state_test.exs │ ├── support │ ├── fake_invoke.ex │ ├── in_memory_client.ex │ └── test_handler.ex │ └── test_helper.exs └── examples └── hello_world ├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib └── hello_world.ex ├── mix.exs ├── rel └── config.exs └── test └── test_helper.exs /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | lambda-*.tar 24 | *.zip 25 | 26 | *.lock 27 | *.swp 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-lambda-elixir-runtime/issues), or [recently closed](https://github.com/aws-samples/aws-lambda-elixir-runtime/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-lambda-elixir-runtime/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-lambda-elixir-runtime/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Lambda Elixir Runtime 2 | 3 | Example implementation of a custom runtime for running Elixir on AWS Lambda. 4 | 5 | ## License Summary 6 | 7 | This sample code is made available under a modified MIT license. See the LICENSE file. 8 | 9 | ``` 10 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 13 | software and associated documentation files (the "Software"), to deal in the Software 14 | without restriction, including without limitation the rights to use, copy, modify, 15 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 16 | permit persons to whom the Software is furnished to do so. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | ``` 25 | 26 | ## Installation 27 | 28 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 29 | by adding `lambda` to your list of dependencies in `mix.exs`: 30 | 31 | ```elixir 32 | def deps do 33 | [ 34 | {:lambda, "~> 0.1.0"} 35 | ] 36 | end 37 | ``` 38 | 39 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 40 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 41 | be found at [https://hexdocs.pm/lambda](https://hexdocs.pm/lambda). 42 | 43 | -------------------------------------------------------------------------------- /elixir_runtime/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 80 5 | ] 6 | -------------------------------------------------------------------------------- /elixir_runtime/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | _build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | .fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | lambda-*.tar 24 | *.zip 25 | 26 | *.lock 27 | *.swp 28 | -------------------------------------------------------------------------------- /elixir_runtime/LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License Summary 2 | 3 | This code is made available under a modified MIT license. See the LICENSE file. 4 | 5 | ``` 6 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | software and associated documentation files (the "Software"), to deal in the Software 10 | without restriction, including without limitation the rights to use, copy, modify, 11 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /elixir_runtime/README.md: -------------------------------------------------------------------------------- 1 | # AWS Lambda Elixir Runtime 2 | 3 | Example implementation of a custom runtime for running Elixir on AWS Lambda. 4 | 5 | ## Installation 6 | 7 | The package can be installed by adding `aws_lambda_elixir_runtime` to your list 8 | of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:aws_lambda_elixir_runtime, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | ## Documentation 19 | 20 | Documentation can be generated with 21 | [ExDoc](https://github.com/elixir-lang/ex_doc). 22 | 23 | ## Step By Step Usage 24 | 25 | This section is a step by step for creating the hello world example. 26 | 27 | First, create a new mix project in a fresh directory: 28 | 29 | ```sh 30 | > mix new --app hello_world ./hello_world 31 | ``` 32 | 33 | Now declare a dependency on `:aws_lambda_elixir_runtime` and 34 | `:distillery`, which is used to package the OTP release. 35 | 36 | Edit `mix.exs`: 37 | 38 | ```elixir 39 | def deps do 40 | [ 41 | {:aws_lambda_elixir_runtime, "~> 0.1.0"}, 42 | {:distillery, "~> 2.0"} 43 | ] 44 | end 45 | ``` 46 | 47 | Now get the dependencies: 48 | 49 | ```sh 50 | > mix deps.get 51 | ``` 52 | 53 | The `:aws_lambda_elixir_runtime` has a mix task which will generate a correct 54 | Distillery release file for deploying to Lambda. This is a one-time setup 55 | for the project because once generated the file can be versioned and customized 56 | like any other release. Generate the file like so: 57 | 58 | ```sh 59 | > mix gen_lambda_release 60 | ``` 61 | 62 | Now the project is ready to be built and deployed -- all that remains is to 63 | actually write a handler function. Open the `lib/hello_world.ex` and edit it 64 | to read: 65 | 66 | ```elixir 67 | defmodule HelloWorld do 68 | 69 | def my_hello_world_handler(request, context) 70 | when is_map(request) and is_map(context) do 71 | """ 72 | Hello World! 73 | Request: #{Kernel.inspect(request)} 74 | Context: #{Kernel.inspect(context)} 75 | """ 76 | |> IO.puts() 77 | 78 | :ok 79 | end 80 | end 81 | ``` 82 | 83 | This just defines a single public function in the HelloWorld module. Any 84 | public function can be used to handle Lambda invocations, it just needs to 85 | accept two maps. 86 | 87 | Now, the project can be built and zipped: 88 | 89 | ```sh 90 | > mix do release, bootstrap, zip 91 | ``` 92 | 93 | The `release` task is the standard Distillery release operation. The 94 | `bootstrap` task generates an executable shell script which is called by the 95 | AWS Lambda service to start the Elixir OTP application. And the `zip` task just 96 | bundles the contents of the Distillery release into a single zip file. 97 | 98 | When this finishes, there should be a `lambda.zip` file in the current 99 | directory. This file can be uploaded to AWS lambda using the AWS console or the 100 | cli. Using the CLI would look like the following: 101 | 102 | ```sh 103 | > aws lambda create-function \ 104 | --region $AWS_REGION \ 105 | --function-name HelloWorld \ 106 | --handler Elixir.HelloWorld:my_hello_world_handler \ 107 | --role $ROLE_ARN \ 108 | --runtime provided \ 109 | --zip-file fileb://./lambda.zip 110 | ``` 111 | 112 | Once created the function can be invoked from the console, the SDK, or the CLI. 113 | Invoking from the CLI would look like this: 114 | 115 | ```sh 116 | > aws lambda invoke \ 117 | --function-name HelloWorld \ 118 | --region $AWS_REGION \ 119 | --lag-type TAIL \ 120 | --payload '{"msg": "a fake request"}' \ 121 | outputfile.txt 122 | ... 123 | 124 | > cat outputfile.txt 125 | Hello World! 126 | Request: %{ "msg" => "a fake request" } 127 | Context: %{ ... } 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /elixir_runtime/config/config.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | use Mix.Config 5 | 6 | config :logger, 7 | :console, 8 | level: :debug, 9 | metadata: [:module, :function, :line] 10 | 11 | # Uncomment to enable environment-specific configuration 12 | # import_config "#{Mix.env()}.exs" 13 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/application.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Application do 5 | @moduledoc """ 6 | The main OTP Application for the Elixir Runtime. 7 | 8 | The application consists of two processes: the runtime loop, and the monitor. 9 | The Runtime process executes the user's code and polls the Lambda Service to 10 | get function invocations. The Monitor watches the Runtime process and reports 11 | any errors to the Lambda Service. 12 | """ 13 | 14 | use Application 15 | 16 | def start(_type, _args) do 17 | children = [ 18 | { 19 | ElixirRuntime.Monitor.Server, 20 | [name: ElixirRuntime.Monitor, client: ElixirRuntime.LambdaServiceClient] 21 | }, 22 | {ElixirRuntime.Loop, [client: ElixirRuntime.LambdaServiceClient]} 23 | ] 24 | 25 | Supervisor.start_link(children, strategy: :one_for_one) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/lambda_service_client.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.LambdaServiceClient do 5 | @moduledoc """ 6 | This module represents an HTTP client for the Lambda Runtime service. 7 | """ 8 | 9 | require Logger 10 | @behaviour ElixirRuntime.Loop.Client 11 | @behaviour ElixirRuntime.Monitor.Client 12 | 13 | alias ElixirRuntime.LambdaServiceClient 14 | 15 | def service_endpoint do 16 | System.get_env("AWS_LAMBDA_RUNTIME_API") 17 | end 18 | 19 | @impl true 20 | @spec invocation_error( 21 | ElixirRuntime.Monitor.Client.error(), 22 | ElixirRuntime.Monitor.Client.id() 23 | ) :: no_return 24 | def invocation_error(err_msg, id) do 25 | url = 26 | 'http://#{service_endpoint()}/2018-06-01/runtime/invocation/#{id}/error' 27 | 28 | request = {url, [], 'text/plain', err_msg} 29 | {:ok, _response} = :httpc.request(:post, request, [], []) 30 | end 31 | 32 | @impl true 33 | @spec init_error(Monitor.Client.error()) :: no_return 34 | def init_error(err_msg) do 35 | url = 'http://#{service_endpoint()}/2018-06-01/runtime/init/error' 36 | request = {url, [], 'text/plain', err_msg} 37 | {:ok, _response} = :httpc.request(:post, request, [], []) 38 | end 39 | 40 | @impl true 41 | @spec complete_invocation( 42 | Runtime.Client.id(), 43 | Runtime.Client.response() 44 | ) :: no_return 45 | def complete_invocation(id, response) do 46 | url = 47 | 'http://#{service_endpoint()}/2018-06-01/runtime/invocation/#{id}/response' 48 | 49 | request = {url, [], 'text/plain', response} 50 | {:ok, _response} = :httpc.request(:post, request, [], []) 51 | end 52 | 53 | @impl true 54 | @spec next_invocation() :: Runtime.Client.invocation() 55 | def next_invocation do 56 | url = 'http://#{service_endpoint()}/2018-06-01/runtime/invocation/next' 57 | response = :httpc.request(:get, {url, []}, [], []) 58 | Logger.debug("Http get from #{url} was #{Kernel.inspect(response)}") 59 | parse(response) 60 | end 61 | 62 | defp parse(_response = {:ok, {{_, 200, _}, headers, body}}) do 63 | context = LambdaServiceClient.Context.from_headers(headers) 64 | {LambdaServiceClient.Context.request_id(context), body, context} 65 | end 66 | 67 | defp parse(_response) do 68 | :no_invocation 69 | end 70 | end 71 | 72 | defmodule ElixirRuntime.LambdaServiceClient.Context do 73 | @request_id "lambda-runtime-aws-request-id" 74 | @trace_id "lambda-runtime-trace-id" 75 | @client_context "x-amz-client-context" 76 | @cognito_identity "x-amz-cognito-identity" 77 | @deadline_ns "lambda-runtime-deadline-ms" 78 | @invoked_function_arn "lambda-runtime-invoked-function-arn" 79 | 80 | @known_headers [ 81 | @request_id, 82 | @trace_id, 83 | @client_context, 84 | @cognito_identity, 85 | @deadline_ns, 86 | @invoked_function_arn 87 | ] 88 | 89 | @spec from_headers([{String.t(), String.t()}]) :: Map.t() 90 | def from_headers(headers) do 91 | headers 92 | |> Enum.map(fn {field, value} -> {to_string(field), to_string(value)} end) 93 | |> Enum.map(fn {field, value} -> {String.downcase(field), value} end) 94 | |> Enum.filter(fn {field, _} -> field in @known_headers end) 95 | |> Map.new() 96 | end 97 | 98 | @spec request_id(Map.t()) :: String.t() 99 | def request_id(context) when is_map(context) do 100 | context[@request_id] 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/loop.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Loop do 5 | @moduledoc """ 6 | The main Runtime loop process. 7 | 8 | This Process is responsible for polling the Lambda Runtime Service for 9 | function invocations and invoking the user's code. If this process crashes 10 | then the Monitor will report the error and stacktrace automatically. 11 | """ 12 | 13 | use Task, restart: :permanent 14 | require Logger 15 | alias __MODULE__ 16 | alias Loop.Handler 17 | alias ElixirRuntime.Monitor 18 | 19 | @type client :: module() 20 | 21 | @doc "spawn a task to run the main loop asynchronously" 22 | def start_link(args \\ []) do 23 | client = Keyword.get(args, :client) 24 | Task.start_link(Loop, :main, [client]) 25 | end 26 | 27 | @doc "the main entrypoint for the runtime" 28 | @spec main(client()) :: no_return 29 | def main(client) do 30 | Monitor.watch(self()) 31 | handler = Handler.configured() 32 | loop(client, handler) 33 | end 34 | 35 | @doc "the runtime's main loop" 36 | def loop(client, handler) do 37 | Monitor.reset() 38 | client.next_invocation() |> process(client, handler) 39 | loop(client, handler) 40 | end 41 | 42 | defp process(:no_invocation, _client, _handler) do 43 | Logger.debug("no invocation to process") 44 | Process.sleep(50) 45 | end 46 | 47 | defp process(invocation = {id, body, context}, client, handler) do 48 | Monitor.started(id) 49 | Logger.info("handle invocation #{Kernel.inspect(invocation)}") 50 | 51 | response = 52 | handler 53 | |> Handler.invoke(Poison.decode!(body), context) 54 | |> Poison.encode!() 55 | 56 | client.complete_invocation(id, response) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/loop/client.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Loop.Client do 5 | @moduledoc "The Lambda Runtime Service Client behavior this runtime requires" 6 | 7 | @type id :: String.t() 8 | @type body :: String.t() 9 | @type context :: Map.t() 10 | @type invocation :: {id, body, context} | :no_invocation 11 | @type response :: String.t() 12 | 13 | @callback next_invocation() :: invocation 14 | @callback complete_invocation(id, response) :: no_return 15 | end 16 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/loop/handler.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Loop.Handler do 5 | @moduledoc """ 6 | This module defines the Handler struct which is used to represent the 7 | module-function atom pair which identifies a client's entrypoint. 8 | """ 9 | 10 | require Logger 11 | alias __MODULE__ 12 | 13 | @enforce_keys [:module, :function] 14 | defstruct [ 15 | :module, 16 | :function 17 | ] 18 | 19 | @doc """ 20 | Manually create a handler from two atoms. 21 | ## Examples 22 | 23 | iex> ElixirRuntime.Loop.Handler.new(Elixir.Example, :handle) 24 | %ElixirRuntime.Loop.Handler{module: Elixir.Example, function: :handle} 25 | """ 26 | def new(module, function) when is_atom(module) and is_atom(function) do 27 | %Handler{module: module, function: function} 28 | end 29 | 30 | @doc """ 31 | Create a handler from the $_HANDLER environment variable. 32 | ## Examples 33 | 34 | iex> System.put_env("_HANDLER", "Elixir.Example:handle") 35 | iex> ElixirRuntime.Loop.Handler.configured() 36 | %ElixirRuntime.Loop.Handler{module: Elixir.Example, function: :handle} 37 | """ 38 | def configured do 39 | [module, function] = handler_string() |> String.split(":", trim: true) 40 | new(String.to_atom(module), String.to_atom(function)) 41 | end 42 | 43 | @doc """ 44 | Invoke the handler function with the body as an argument. 45 | ## Examples 46 | 47 | Create a handler for String.trim and invoke it to get a result. 48 | iex> defmodule Example, do: def func(body, _context), do: body 49 | iex> handler = ElixirRuntime.Loop.Handler.new(Example, :func) 50 | iex> handler |> ElixirRuntime.Loop.Handler.invoke(%{msg: "hello"}, %{}) 51 | %{msg: "hello"} 52 | """ 53 | def invoke(%Handler{module: module, function: function}, body, context) 54 | when is_map(body) and is_map(context) do 55 | Logger.info("invoke #{module}.#{function}(#{Kernel.inspect(body)})") 56 | Kernel.apply(module, function, [body, context]) 57 | end 58 | 59 | defp handler_string do 60 | raw = System.get_env("_HANDLER") 61 | Logger.debug("found handler string '#{raw}'") 62 | raw 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/loop/monitor.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Loop.Monitor do 5 | @moduledoc """ 6 | The Runtime requires a stateful monitor which will observe any 7 | failures and call the proper backend APIs. 8 | """ 9 | 10 | @doc "Tell the monitor to start watching this process" 11 | @callback watch(pid()) :: no_return 12 | 13 | @doc """ 14 | Tell the monitor that the runtime has started processing an invocation. 15 | """ 16 | @callback started(Runtime.Client.id()) :: no_return 17 | 18 | @doc """ 19 | Tell the monitor that the runtime is starting over fresh and no 20 | invocation is in progress. 21 | """ 22 | @callback reset() :: no_return 23 | end 24 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/monitor.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor do 5 | @moduledoc """ 6 | The monitor is responsible for reporting errors in the Elixir process to 7 | the AWS Lambda runtime service. 8 | The monitor is a stateful process which tracks the request ID for the 9 | currently-executing function. 10 | """ 11 | 12 | @behaviour ElixirRuntime.Loop.Monitor 13 | 14 | @doc """ 15 | Tell the monitor server to watch the given process. 16 | """ 17 | @impl true 18 | def watch(monitor \\ ElixirRuntime.Monitor, process) when is_pid(process) do 19 | GenServer.call(monitor, {:watch, process}) 20 | end 21 | 22 | @doc """ 23 | Reset the monitor back to it's initial state, forgetting any currently-known 24 | ingestor IDs. 25 | """ 26 | @impl true 27 | def reset(monitor \\ ElixirRuntime.Monitor) do 28 | GenServer.call(monitor, :reset) 29 | end 30 | 31 | @doc """ 32 | Notify the monitor that the runtime loop has started processing an invocation. 33 | """ 34 | @impl true 35 | def started(monitor \\ ElixirRuntime.Monitor, id) do 36 | GenServer.call(monitor, {:start_invocation, id}) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/monitor/client.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor.Client do 5 | @moduledoc """ 6 | This module defines the client behavior required by the runtime monitor. 7 | """ 8 | 9 | @type t :: module() 10 | @type error :: String.t() 11 | @type id :: String.t() 12 | 13 | @callback init_error(error()) :: no_return 14 | @callback invocation_error(error(), id()) :: no_return 15 | end 16 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/monitor/error.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor.Error do 5 | @moduledoc """ 6 | This module defines the Error struct which is used to communicate runtime 7 | errors to the Lambda Runtime Service. 8 | """ 9 | 10 | alias __MODULE__ 11 | 12 | @derive Poison.Encoder 13 | defstruct [ 14 | :errorMessage, 15 | :errorType, 16 | :stackTrace 17 | ] 18 | 19 | @type error_type :: :function | :runtime 20 | @type error :: %Error{ 21 | errorMessage: String.t(), 22 | errorType: String.t(), 23 | stackTrace: list 24 | } 25 | 26 | @doc "Build an Error from a structured process-exit reason" 27 | @spec from_exit_reason(error_type, {atom(), list()}) :: error 28 | def from_exit_reason(error_type, _reason = {error, stacktrace}) do 29 | exception = Exception.normalize(:error, error, stacktrace) 30 | build_error(error_name(error_type, exception), exception, stacktrace) 31 | end 32 | 33 | @doc "Build an Error from an unknown process-exit reason" 34 | @spec from_exit_reason(error_type, term) :: error 35 | def from_exit_reason(error_type, reason) do 36 | exception = Exception.normalize(:error, {"unexpected exit", reason}) 37 | build_error(error_name(error_type, exception), exception, []) 38 | end 39 | 40 | defp error_name(:function, %{__struct__: name, __exception__: true}) do 41 | "Function#{name}" 42 | end 43 | 44 | defp error_name(:runtime, %{__struct__: name, __exception__: true}) do 45 | "Runtime#{name}" 46 | end 47 | 48 | defp error_name(:function, _) do 49 | "FunctionUnknownError" 50 | end 51 | 52 | defp error_name(:runtime, _) do 53 | "RuntimeUnknownError" 54 | end 55 | 56 | defp build_error(error_type, err, stacktrace) do 57 | %Error{ 58 | errorMessage: Exception.format(:error, err), 59 | errorType: error_type, 60 | stackTrace: Enum.map(stacktrace, &Exception.format_stacktrace_entry/1) 61 | } 62 | end 63 | 64 | # Old Code 65 | 66 | @doc "Build an Error struct from a runtime error" 67 | def from(error_type, err, stacktrace) do 68 | build_error(error_name(error_type, err), err, stacktrace) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/monitor/server.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor.Server do 5 | use GenServer 6 | 7 | alias __MODULE__ 8 | alias ElixirRuntime.Monitor.State, as: State 9 | 10 | def start_link(args) do 11 | client = Keyword.get(args, :client) 12 | GenServer.start_link(Server, client, args) 13 | end 14 | 15 | @impl true 16 | def init(client) do 17 | {:ok, State.initial(client)} 18 | end 19 | 20 | @impl true 21 | @spec handle_call({:watch, pid}, pid, State.monitor_state()) :: term() 22 | def handle_call({:watch, pid}, _from, state) do 23 | Process.monitor(pid) 24 | {:reply, :ok, state} 25 | end 26 | 27 | @impl true 28 | @spec handle_call({atom(), String.t()}, pid, State.monitor_state()) :: term 29 | def handle_call({:start_invocation, id}, _from, state) do 30 | {:reply, :ok, State.start_invocation(state, id)} 31 | end 32 | 33 | @impl true 34 | @spec handle_call(:reset, pid, State.monitor_state()) :: term 35 | def handle_call(:reset, _from, state) do 36 | {:reply, :ok, State.reset(state)} 37 | end 38 | 39 | @impl true 40 | def handle_info({:DOWN, _ref, :process, _pid, :normal}, state) do 41 | {:noreply, State.reset(state)} 42 | end 43 | 44 | @impl true 45 | def handle_info({:DOWN, _ref, :process, _pid, reason}, state) do 46 | State.error(state, reason) 47 | {:noreply, State.reset(state)} 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /elixir_runtime/lib/elixir_runtime/monitor/state.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor.State do 5 | alias ElixirRuntime.Monitor 6 | 7 | @type monitor_state :: 8 | {:not_started, Monitor.Client.t()} 9 | | {:in_progress, Monitor.Client.id(), Monitor.Client.t()} 10 | 11 | @doc "the monitor's initial state" 12 | @spec initial(Monitor.Client.t()) :: monitor_state 13 | def initial(client) do 14 | {:not_started, client} 15 | end 16 | 17 | @doc "start processing an invocation" 18 | @spec start_invocation(atom(), Monitor.Client.id()) :: monitor_state 19 | def start_invocation({:not_started, client}, invocation_id) do 20 | {:in_progress, invocation_id, client} 21 | end 22 | 23 | @doc "report an error before an invocation was started" 24 | @spec error(monitor_state, term()) :: no_return 25 | def error({:not_started, client}, reason) do 26 | Monitor.Error.from_exit_reason(:runtime, reason) 27 | |> Poison.encode!() 28 | |> client.init_error() 29 | end 30 | 31 | @doc "report an error before an invocation was started" 32 | @spec error(monitor_state, term()) :: no_return 33 | def error({:in_progress, id, client}, reason) do 34 | Monitor.Error.from_exit_reason(:function, reason) 35 | |> Poison.encode!() 36 | |> client.invocation_error(id) 37 | end 38 | 39 | @doc "reset an existing state back to the initial value" 40 | @spec reset(monitor_state) :: monitor_state 41 | def reset(from_state) do 42 | from_state 43 | |> client() 44 | |> initial() 45 | end 46 | 47 | defp client({:in_progress, _, client}), do: client 48 | defp client({:not_started, client}), do: client 49 | end 50 | -------------------------------------------------------------------------------- /elixir_runtime/lib/epmd/stub_client.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule EPMD.StubClient do 5 | @moduledoc """ 6 | This module implements an EPMD client which does not actually coordinate 7 | with the real EPMD or allow the current node to communicate with other 8 | nodes. 9 | """ 10 | 11 | @doc """ 12 | No need to start a process because the stub client has no state. 13 | """ 14 | def start_link do 15 | :ignore 16 | end 17 | 18 | @doc "family is ignored" 19 | def register_node(name, port, _family) do 20 | register_node(name, port) 21 | end 22 | 23 | @doc "return :ok and a random number" 24 | def register_node(_name, _port) do 25 | {:ok, :rand.uniform(3)} 26 | end 27 | 28 | @doc """ 29 | Return a hardcoded port and version. 30 | If other nodes were going to be supported this would need to be changed. 31 | """ 32 | def port_please(_name, _ip) do 33 | port = 4370 34 | version = 5 35 | {:port, port, version} 36 | end 37 | 38 | @doc """ 39 | There's no way to get the names of all running nodes because this client 40 | doesn't actually connect to an EPMD instance. 41 | """ 42 | def names(_hostname) do 43 | {:error, :address} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /elixir_runtime/lib/mix/tasks/bootstrap.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule Mix.Tasks.Bootstrap do 5 | @moduledoc """ 6 | Generate a bootstrap script for the project in the release directory. 7 | This task will fail if it's run before `mix release`. 8 | """ 9 | 10 | use Mix.Task 11 | 12 | @runtime_libs "elixir_runtime-0.1.0/priv" 13 | 14 | @shortdoc "Generate a bootstrap script for the project" 15 | def run(_) do 16 | name = 17 | Mix.Project.config() 18 | |> Keyword.fetch!(:app) 19 | |> to_string 20 | 21 | path = "_build/#{Mix.env()}/rel/#{name}/bootstrap" 22 | 23 | Mix.Generator.create_file(path, bootstrap(name)) 24 | File.chmod!(path, 0o777) 25 | end 26 | 27 | # The bootstrap script contents 28 | defp bootstrap(app) when is_binary(app) do 29 | """ 30 | \#!/bin/bash 31 | 32 | set -x 33 | 34 | BASE=$(dirname "$0") 35 | EXE=$BASE/bin/#{app} 36 | 37 | HOME=/tmp 38 | export HOME 39 | 40 | \# So that distillery doesn't try to write any files 41 | export RELEASE_READ_ONLY=true 42 | 43 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$BASE/lib/#{@runtime_libs} 44 | 45 | $EXE foreground 46 | """ 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /elixir_runtime/lib/mix/tasks/gen_lambda_release.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule Mix.Tasks.GenLambdaRelease do 5 | @moduledoc """ 6 | Generate a distillery release configuration file for lambda release builds. 7 | """ 8 | 9 | use Mix.Task 10 | 11 | @shortdoc "Generate a distillery release for AWS Lambda" 12 | def run(_) do 13 | name = 14 | Mix.Project.config() 15 | |> Keyword.fetch!(:app) 16 | |> to_string 17 | 18 | Mix.Generator.create_file("rel/config.exs", distillery_config(name)) 19 | end 20 | 21 | defp distillery_config(app) do 22 | """ 23 | ~w(rel plugins *.exs) 24 | |> Path.join() 25 | |> Path.wildcard() 26 | |> Enum.map(&Code.eval_file(&1)) 27 | 28 | use Mix.Releases.Config, 29 | default_release: :#{app}, 30 | default_environment: :lambda 31 | 32 | environment :lambda do 33 | set include_erts: true 34 | set include_src: false 35 | set cookie: :test 36 | set include_system_libs: true 37 | 38 | \# Distillery forces the ERTS into 'distributed' mode which will 39 | \# attempt to connect to EPMD. This is not supported behavior in the 40 | \# AWS Lambda runtime because our process isn't allowed to connect to 41 | \# other ports on this host. 42 | \# 43 | \# So '-start_epmd false' is set so the ERTS doesn't try to start EPMD. 44 | \# And '-epmd_module' is set to use a no-op implementation of EPMD 45 | set erl_opts: "-start_epmd false -epmd_module Elixir.EPMD.StubClient" 46 | end 47 | 48 | release :#{app} do 49 | set version: current_version(:#{app}) 50 | set applications: [ 51 | :runtime_tools, :aws_lambda_elixir_runtime 52 | ] 53 | end 54 | """ 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /elixir_runtime/lib/mix/tasks/zip.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule Mix.Tasks.Zip do 5 | use Mix.Task 6 | 7 | @shortdoc "zip the contents of the current release" 8 | def run(_) do 9 | path = release_path(app_name()) 10 | 11 | cmd = "cd #{path} && zip -r lambda.zip * && cp lambda.zip #{System.cwd()}" 12 | 13 | System.cmd("sh", ["-c", cmd]) 14 | end 15 | 16 | defp app_name() do 17 | Mix.Project.config() 18 | |> Keyword.fetch!(:app) 19 | |> to_string 20 | end 21 | 22 | defp release_path(app) do 23 | "_build/#{Mix.env()}/rel/#{app}/" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /elixir_runtime/mix.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule Lambda.MixProject do 5 | use Mix.Project 6 | 7 | def project do 8 | [ 9 | app: :aws_lambda_elixir_runtime, 10 | version: "0.1.0", 11 | elixir: "~> 1.7", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | aliases: [test: "test --no-start"], 16 | 17 | # Docs 18 | name: "AWS Lambda Elixir Runtime", 19 | source_url: "https://github.com/aws-samples/aws-lambda-elixir-runtime", 20 | homepage_url: "https://github.com/aws-samples/aws-lambda-elixir-runtime/tree/master/elixir_runtime", 21 | docs: [ 22 | source_url_pattern: 23 | "https://github.com/aws-samples/aws-lambda-elixir-runtime/blob/master/elixir_runtime/%{path}#L%{line}", 24 | main: "readme", 25 | extras: [ 26 | "README.md", 27 | "LICENSE.md" 28 | ] 29 | ] 30 | ] 31 | end 32 | 33 | # Run "mix help compile.app" to learn about applications. 34 | def application do 35 | [ 36 | mod: {ElixirRuntime.Application, []}, 37 | extra_applications: [:logger, :inets] 38 | ] 39 | end 40 | 41 | # Run "mix help deps" to learn about dependencies. 42 | defp deps do 43 | [ 44 | {:poison, "~> 3.1"}, 45 | {:mox, "~> 0.4", only: :test}, 46 | {:ex_doc, "~> 0.19", only: :dev, runtime: false} 47 | ] 48 | end 49 | 50 | defp elixirc_paths(:test) do 51 | ["lib", "test/support"] 52 | end 53 | 54 | defp elixirc_paths(_) do 55 | ["lib"] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /elixir_runtime/priv/libcrypto.so.1.0.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-elixir-runtime/b5a7e664dbad7bc6832d5ea7989de6df813435bc/elixir_runtime/priv/libcrypto.so.1.0.0 -------------------------------------------------------------------------------- /elixir_runtime/priv/libssl.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-elixir-runtime/b5a7e664dbad7bc6832d5ea7989de6df813435bc/elixir_runtime/priv/libssl.so -------------------------------------------------------------------------------- /elixir_runtime/test/elixir_runtime/application_test.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Application.Test do 5 | use ExUnit.Case 6 | 7 | # This test suite is for the end-to-end behavior of the elixir runtime 8 | # application. The tests leverage an in-memory client which echos requests 9 | # back to the test process for validation. 10 | 11 | def echo_msg(_request = %{"msg" => message}, _context), do: message 12 | 13 | defmodule MyCustomBug do 14 | defexception [:message] 15 | end 16 | 17 | def logical_bug_handler(_body, _context) do 18 | raise MyCustomBug, "a problem" 19 | end 20 | 21 | setup do 22 | # clear the _HANDLER var before and after each test 23 | System.delete_env("_HANDLER") 24 | on_exit(fn -> System.delete_env("_HANDLER") end) 25 | [] 26 | end 27 | 28 | test "a single successful invocation" do 29 | invocation = Support.FakeInvoke.with_message("hello world") 30 | expected_id = Support.FakeInvoke.id(invocation) 31 | 32 | System.put_env("_HANDLER", "Elixir.ElixirRuntime.Application.Test:echo_msg") 33 | start_with_invocations([invocation]) 34 | 35 | assert_receive {:next, ^invocation} 36 | assert_receive {:complete, ^expected_id, "\"hello world\""} 37 | end 38 | 39 | test "a handler with a logical error" do 40 | invoke = Support.FakeInvoke.with_message("some message") 41 | expected_id = Support.FakeInvoke.id(invoke) 42 | 43 | System.put_env( 44 | "_HANDLER", 45 | "Elixir.ElixirRuntime.Application.Test:logical_bug_handler" 46 | ) 47 | 48 | start_with_invocations([invoke]) 49 | 50 | assert_receive {:invocation_error, msg, ^expected_id} 51 | 52 | assert String.contains?( 53 | msg, 54 | "FunctionElixir.ElixirRuntime.Application.Test.MyCustomBug" 55 | ) 56 | end 57 | 58 | test "a missing handler string" do 59 | start_with_invocations([Support.FakeInvoke.with_message("not used")]) 60 | 61 | assert_receive {:init_error, msg}, 500 62 | assert String.contains?(msg, "RuntimeElixir.FunctionClauseError") 63 | end 64 | 65 | test "a malformed handler string" do 66 | System.put_env("_HANDLER", "Elixir.Application.Test.this_isnt_right") 67 | start_with_invocations([Support.FakeInvoke.with_message("not used")]) 68 | 69 | assert_receive {:init_error, msg} 70 | assert String.contains?(msg, "RuntimeElixir.MatchError") 71 | end 72 | 73 | test "a missing handler implementation" do 74 | invoke = Support.FakeInvoke.with_message("I'll never be handled") 75 | expected_id = Support.FakeInvoke.id(invoke) 76 | 77 | System.put_env("_HANDLER", "Elixir.Application.Test:doesnt_exist") 78 | start_with_invocations([invoke]) 79 | 80 | assert_receive {:invocation_error, msg, ^expected_id} 81 | assert String.contains?(msg, "FunctionElixir.UndefinedFunctionError") 82 | end 83 | 84 | # Launch the components of the application 85 | # 1. start the InMemoryClient with the provided invocations, if any 86 | # 2. start the Monitor.Server named Monitor 87 | # 3. start the Runtime task 88 | # This is the same process as starting the application but it's done 89 | # piece-by-piece here so the InMemoryClient can be passed into the Runtime 90 | # and monitor. 91 | defp start_with_invocations(invocations) do 92 | start_supervised!( 93 | {Support.InMemoryClient, %{pending: invocations, listener: self()}} 94 | ) 95 | 96 | start_supervised!({ 97 | ElixirRuntime.Monitor.Server, 98 | [client: Support.InMemoryClient, name: ElixirRuntime.Monitor] 99 | }) 100 | 101 | start_supervised!({ElixirRuntime.Loop, [client: Support.InMemoryClient]}) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /elixir_runtime/test/elixir_runtime/loop/handler_test.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Loop.Handler.Test do 5 | use ExUnit.Case 6 | doctest ElixirRuntime.Loop.Handler 7 | end 8 | -------------------------------------------------------------------------------- /elixir_runtime/test/elixir_runtime/monitor/error_test.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor.Error.Test do 5 | use ExUnit.Case, async: true 6 | doctest ElixirRuntime.Monitor.Error 7 | 8 | alias ElixirRuntime.Monitor.Error 9 | 10 | setup do 11 | {:current_stacktrace, stacktrace} = 12 | Process.info(self(), :current_stacktrace) 13 | 14 | formatted = Enum.map(stacktrace, &Exception.format_stacktrace_entry/1) 15 | 16 | [stacktrace: stacktrace, formatted: formatted] 17 | end 18 | 19 | test "create an error from a function exception", context do 20 | ex = %RuntimeError{message: "msg"} 21 | error = Error.from(:function, ex, context.stacktrace) 22 | 23 | assert error.errorMessage === Exception.format(:error, ex) 24 | assert error.errorType === "FunctionElixir.RuntimeError" 25 | assert error.stackTrace === context.formatted 26 | end 27 | 28 | test "create an error from a function exit reason", context do 29 | reason = {:badarg, context.stacktrace} 30 | error = Error.from_exit_reason(:function, reason) 31 | 32 | assert error.errorMessage === Exception.format(:error, %ArgumentError{}) 33 | assert error.errorType === "FunctionElixir.ArgumentError" 34 | assert error.stackTrace === context.formatted 35 | end 36 | 37 | test "create an error from a runtime exit reason", context do 38 | reason = {:badarg, context.stacktrace} 39 | error = Error.from_exit_reason(:runtime, reason) 40 | 41 | assert error.errorMessage === Exception.format(:error, %ArgumentError{}) 42 | assert error.errorType === "RuntimeElixir.ArgumentError" 43 | assert error.stackTrace === context.formatted 44 | end 45 | 46 | test "create an error from an unstructured function exit reason" do 47 | reason = :kill 48 | 49 | expected_exception = 50 | Exception.normalize(:error, {"unexpected exit", reason}) 51 | 52 | error = Error.from_exit_reason(:function, reason) 53 | assert error.errorMessage === Exception.format(:error, expected_exception) 54 | assert error.errorType === "FunctionElixir.ErlangError" 55 | assert error.stackTrace === [] 56 | end 57 | 58 | test "create an error from an unstructured runtime exit reason" do 59 | reason = :kill 60 | 61 | expected_exception = 62 | Exception.normalize(:error, {"unexpected exit", reason}) 63 | 64 | error = Error.from_exit_reason(:runtime, reason) 65 | assert error.errorMessage === Exception.format(:error, expected_exception) 66 | assert error.errorType === "RuntimeElixir.ErlangError" 67 | assert error.stackTrace === [] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /elixir_runtime/test/elixir_runtime/monitor/state_test.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule ElixirRuntime.Monitor.State.Test do 5 | use ExUnit.Case, async: true 6 | doctest ElixirRuntime.Monitor.State 7 | 8 | alias ElixirRuntime.Monitor 9 | alias Monitor.State 10 | 11 | defmodule FakeClient do 12 | @behaviour ElixirRuntime.Monitor.Client 13 | 14 | @impl true 15 | def init_error(err_msg) do 16 | send(self(), {:init_error, err_msg}) 17 | end 18 | 19 | @impl true 20 | def invocation_error(err_msg, id) do 21 | send(self(), {:invocation_error, err_msg, id}) 22 | end 23 | end 24 | 25 | test "the monitor's initial state" do 26 | assert State.initial(FakeClient) === {:not_started, FakeClient} 27 | end 28 | 29 | test "starting an invocation from the initial state" do 30 | expected_id = "fakeid" 31 | 32 | result = 33 | State.initial(FakeClient) 34 | |> State.start_invocation(expected_id) 35 | 36 | assert result === {:in_progress, expected_id, FakeClient} 37 | end 38 | 39 | test "report an error from initial state" do 40 | reason = {:badarg, []} 41 | expected = Poison.encode!(Monitor.Error.from_exit_reason(:runtime, reason)) 42 | 43 | State.initial(FakeClient) |> State.error(reason) 44 | 45 | assert_receive {:init_error, ^expected} 46 | end 47 | 48 | test "report an error while an invocation is in progress" do 49 | invoke_id = "fakeid" 50 | reason = {:badarg, []} 51 | expected = Poison.encode!(Monitor.Error.from_exit_reason(:function, reason)) 52 | 53 | State.initial(FakeClient) 54 | |> State.start_invocation(invoke_id) 55 | |> State.error(reason) 56 | 57 | assert_receive {:invocation_error, ^expected, ^invoke_id} 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /elixir_runtime/test/support/fake_invoke.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule Support.FakeInvoke do 5 | @moduledoc """ 6 | This module defines functions for generating and manipulating fake 7 | invocations. 8 | """ 9 | 10 | def with_message(message) do 11 | body = %{msg: message} 12 | {generated_id(), Poison.encode!(body), %{}} 13 | end 14 | 15 | def id({id, _body, _context}), do: id 16 | def body({_id, body, _context}), do: body 17 | 18 | defp generated_id do 19 | id = Integer.to_string(:rand.uniform(100_000_000), 32) 20 | "TestId-#{id}" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /elixir_runtime/test/support/in_memory_client.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule Support.InMemoryClient do 5 | @moduledoc """ 6 | This module implements the ElixirRuntime.Monitor.Client and 7 | ElixirRuntime.Loop.Client behaviours and is used for testing. 8 | This client can hold a pending invocation in memory and serve it to the 9 | runtime for processing. 10 | """ 11 | 12 | alias __MODULE__ 13 | use GenServer 14 | @behaviour ElixirRuntime.Loop.Client 15 | @behaviour ElixirRuntime.Monitor.Client 16 | 17 | @doc "Start the InMemoryClient server" 18 | def start_link(args) do 19 | GenServer.start_link(InMemoryClient, args, name: InMemoryClient) 20 | end 21 | 22 | @impl ElixirRuntime.Loop.Client 23 | def next_invocation do 24 | GenServer.call(InMemoryClient, :next) 25 | end 26 | 27 | @impl ElixirRuntime.Loop.Client 28 | def complete_invocation(id, response) do 29 | GenServer.call(InMemoryClient, {:complete, id, response}) 30 | end 31 | 32 | @impl ElixirRuntime.Monitor.Client 33 | def init_error(err_msg) do 34 | GenServer.call(InMemoryClient, {:init_error, err_msg}) 35 | end 36 | 37 | @impl ElixirRuntime.Monitor.Client 38 | def invocation_error(err_msg, id) do 39 | GenServer.call(InMemoryClient, {:invocation_error, err_msg, id}) 40 | end 41 | 42 | # GenServer Callbacks 43 | 44 | @impl true 45 | def init(args = %{listener: listener, pending: pending}) 46 | when is_pid(listener) and is_list(pending) do 47 | {:ok, args} 48 | end 49 | 50 | @impl true 51 | def handle_call(:next, _from, state = %{pending: []}) do 52 | send(state.listener, {:next, :no_invocation}) 53 | {:reply, :no_invocation, state} 54 | end 55 | 56 | @impl true 57 | def handle_call(:next, _from, state = %{pending: [next | remaining]}) do 58 | send(state.listener, {:next, next}) 59 | {:reply, next, Map.replace!(state, :pending, remaining)} 60 | end 61 | 62 | @impl true 63 | def handle_call(msg, _from, state) do 64 | send(state.listener, msg) 65 | {:reply, :ok, state} 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /elixir_runtime/test/support/test_handler.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule TestHandler do 5 | def echo(body, _context) do 6 | body["msg"] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /elixir_runtime/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | Mox.defmock(FakeRuntimeClient, for: ElixirRuntime.Loop.Client) 5 | Mox.defmock(FakeMonitorClient, for: ElixirRuntime.Monitor.Client) 6 | 7 | Application.ensure_all_started(:mox) 8 | 9 | ExUnit.start() 10 | -------------------------------------------------------------------------------- /examples/hello_world/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 80 5 | ] 6 | -------------------------------------------------------------------------------- /examples/hello_world/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hello_world-*.tar 24 | 25 | -------------------------------------------------------------------------------- /examples/hello_world/README.md: -------------------------------------------------------------------------------- 1 | # HelloWorld Example 2 | 3 | A bare-minimum Lambda function written in Elixir. 4 | 5 | ## Deployment Instructions 6 | 7 | This example is ready to deploy. It needs to be built and bundled: 8 | 9 | ``` 10 | > mix deps.get 11 | > mix do release, bootstrap, zip 12 | ``` 13 | 14 | This creates a file called ```lambda.zip``` in the current directory. 15 | Use the AWS CLI to create the function like so: 16 | 17 | 18 | ``` 19 | > aws lambda create-function \ 20 | --region $AWS_REGION \ 21 | --function-name HelloWorld \ 22 | --handler Elixir.HelloWorld:hello_world \ 23 | --role $ROLE_ARN \ 24 | --runtime provided \ 25 | --zip-file fileb://./lambda.zip 26 | ``` 27 | 28 | This requires that you have already set the ```AWS_REGION``` and ```ROLE_ARN``` 29 | environment variables. Alternatively, the zip can be used directly in the 30 | AWS Console (just navigate to Lambda and Create Function). 31 | 32 | Note that the *handler string* is in the format ```module:function```. Also, 33 | remember that Elixir modules have the ```Elixir.``` prefix to prevent clashes 34 | with Erlang modules. 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/hello_world/config/config.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | use Mix.Config 5 | -------------------------------------------------------------------------------- /examples/hello_world/lib/hello_world.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule HelloWorld do 5 | @moduledoc """ 6 | Entrypoint for my hello world Lambda function. 7 | """ 8 | 9 | require Logger 10 | 11 | @doc """ 12 | The lambda entrypoint is just a public function in a module which accepts 13 | two maps. 14 | The returned term will be passed to Poison for Json Encoding. 15 | """ 16 | @spec hello_world(Map.t(), Map.t()) :: Term 17 | def hello_world(request, context) when is_map(request) and is_map(context) do 18 | """ 19 | Hello World! 20 | Got reqeust #{Kernel.inspect(request)} 21 | Got Context #{Kernel.inspect(context)} 22 | """ 23 | |> Logger.info() 24 | 25 | :ok 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/hello_world/mix.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.ja 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | defmodule HelloWorld.MixProject do 5 | use Mix.Project 6 | 7 | def project do 8 | [ 9 | app: :hello_world, 10 | version: "0.1.0", 11 | elixir: "~> 1.7", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Run "mix help compile.app" to learn about applications. 18 | def application do 19 | [ 20 | extra_applications: [:logger] 21 | ] 22 | end 23 | 24 | # Run "mix help deps" to learn about dependencies. 25 | defp deps do 26 | [ 27 | {:aws_lambda_elixir_runtime, path: "../../elixir_runtime"}, 28 | {:distillery, "~> 2.0"} 29 | ] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/hello_world/rel/config.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ~w(rel plugins *.exs) 5 | |> Path.join() 6 | |> Path.wildcard() 7 | |> Enum.map(&Code.eval_file(&1)) 8 | 9 | use Mix.Releases.Config, 10 | default_release: :hello_world, 11 | default_environment: :lambda 12 | 13 | environment :lambda do 14 | set include_erts: true 15 | set include_src: false 16 | set cookie: :test 17 | set include_system_libs: true 18 | 19 | # Distillery forces the ERTS into 'distributed' mode which will 20 | # attempt to connect to EPMD. This is not supported behavior in the 21 | # AWS Lambda runtime because our process isn't allowed to connect to 22 | # other ports on this host. 23 | # 24 | # So '-start_epmd false' is set so the ERTS doesn't try to start EPMD. 25 | # And '-epmd_module' is set to use a no-op implementation of EPMD 26 | set erl_opts: "-start_epmd false -epmd_module Elixir.EPMD.StubClient" 27 | end 28 | 29 | release :hello_world do 30 | set version: current_version(:hello_world) 31 | set applications: [ 32 | :runtime_tools, :aws_lambda_elixir_runtime 33 | ] 34 | end 35 | -------------------------------------------------------------------------------- /examples/hello_world/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ExUnit.start() 5 | --------------------------------------------------------------------------------