AWS Compute Blog
Ruby 3.2 runtime now available in AWS Lambda
This post is written by Praveen Koorse, Senior Solutions Architect, AWS.
AWS Lambda now supports Ruby 3.2 runtime. With this release, Ruby developers can now take advantage of new features and improvements introduced in Ruby 3 when creating serverless applications on Lambda. Use this runtime today by specifying the runtime parameter of ruby3.2 when creating or updating Lambda functions.
Ruby 3.2 adds many features and performance improvements, including anonymous arguments passing improvements, ‘endless’ methods, Regexp improvements, a new Data class, support for pattern-matching in Time and MatchData, and support for ‘find pattern’ in pattern matching.
Our testing shows Ruby 3.2 cold starts are marginally slower than Ruby 2.7 for a trivial ‘hello world’ function. However, for many real-world workloads, the improved execution performance of Ruby 3.2 results in similar or better performance overall.
Existing Lambda customers using the Ruby 2.7 runtime should migrate to the Ruby 3.2 runtime as soon as possible. Even though community support for Ruby 2.7 has ended, Lambda has extended support for the Ruby 2.7 runtime until December 7, 2023 to provide existing Ruby customers with time to transition to Ruby 3.2. Functions using Ruby 2.7 continue to be eligible for technical support and Lambda will continue to apply OS security updates to the runtime until this date.
Anonymous arguments passing improvements
Ruby 3.2 has introduced improvements to how you can pass anonymous arguments, making it easier and cleaner to work with keyword arguments in code. Previously, you could pass anonymous keyword arguments to a method by using the delegation syntax (…
) or use Module#ruby2_keywords
and delegate *args
, &block
. This method was not intuitive and lacked clarity when working with multiple arguments.
def foo(...)
target(...)
end
ruby2_keywords def foo(*args, &block)
target(*args, &block)
end
Now, if a method declaration includes anonymous positional or keyword arguments, those can be passed to the next method as arguments themselves. The same advantages of anonymous block forwarding apply to rest and keyword rest argument forwarding.
def keywords(**) # accept keyword arguments
foo(**) # pass them to the next method
end
def positional(*) # accept positional arguments
bar(*) # pass to the next method
end
def positional_keywords(*, **) # same as ...
foobar(*, **)
end
Endless methods
Ruby 3 introduced endless methods that enable developers to define methods of exactly one statement with a syntax def method() = statement
. The syntax doesn’t need an end
and allows methods to be defined as short-cut one liners, making the creation of basic utility methods easier and helping developers write clean code and improve readability and maintainability of code.
def dbg_args(a, b=1, c:, d: 6, &block) = puts("Args passed: #{[a, b, c, d, block.call]}")
dbg_args(0, c: 5) { 7 }
# Prints: Args passed: [0, 1, 5, 6, 7]
def square(x) = x**2
square(100)
# => 10000
Regexp improvements
Regular expression matching can take an unexpectedly long time. If your code attempts to match a possibly inefficient Regexp against an untrusted input, an attacker may exploit it for efficient denial of service (so-called regular expression DoS, or ReDoS).
Ruby 3.2 introduces two improvements to mitigate this.
The first is an improved Regexp matching algorithm using a cache-based approach that allows most Regexp matching to be completed in linear time, thus significantly improving overall match performance.
As the prior optimization cannot be applied to some regular expressions, such as those using advanced features or working with a huge fixed number of repetitions, a Regexp timeout improvement now allows you to specify a timeout for Regexp operations as a fallback measure.
There are two APIs to set timeout:
Timeout.timeout
: This is a global configuration and applies to all Regexps in the process.timeout
keyword forRegexp.new
: This allows you to specify a different timeout setting for some Regexps. If used, it takes precedence over the global configuration.
Regexp.timeout = 2.0 # Global configuration set to two seconds
/^x*y?x*()\1$/ =~ "x" * 45000 + "a"
#=> Regexp::TimeoutError is raised in two seconds
my_long_rexp = Regexp.new('^x*y?x*()\1$', timeout: 4)
my_long_rexp =~ "x" * 45000 + "a"
# Regexp::TimeoutError is raised in four seconds
Data class
Ruby 3.2 introduces a new core class – ‘Data’ – to represent simple value-alike objects. The class is similar to Struct and partially shares the implementation, but is intended to be immutable with a more modern interface.
Once a Data class is defined using Data.define
, both positional and keyword arguments can be used while constructing objects.
def handler(event:, context:)
employee = Data.define(:firstname, :lastname, :empid, :department)
emp1 = employee.new('John', 'Doe', 12345, 'Sales')
emp2 = employee.new(firstname: 'Alice', lastname: 'Doe', empid: 12346, department: 'Marketing')
# Alternative form to construct an object
emp3 = employee['Jack', 'Frost', 12354, 'Tech']
emp4 = employee[firstname: 'Emma', lastname: 'Frost', empid: 12453, department: 'HR']
end
Pattern matching improvements
Pattern matching is a feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables.
Ruby 3.2 introduces ‘Find pattern’, allowing you to check if the given object has any elements that match a pattern. This pattern is similar to the Array pattern, except that it finds the first match in a given object containing multiple elements.
For example, previously, if you used the following in a Lambda function:
person = {name: "John", children: [{name: "Mark", age: 12}, {name: "Butler", age: 9}], siblings: [{name: "Mary", age: 31}, {name: "Conrad", age: 38}] }
case person
in {name: "John", children: [{name: "Mark", age: age}]}
p age
end
This wouldn’t match because it does not search for the element in the ‘children’ array. It only matches for an array with a single element named “Mark”.
To find an element in an array with multiple elements, use ‘find pattern’:
case person
in {name: "John", children: [*, {name: "Mark", age: age}, *]}
p age
end
As part of an effort to make core classes more pattern matching friendly, Ruby 3.2 also introduces the ability to deconstruct keys in Time and MatchData, allowing their use in case/in
statements for pattern matching.
# `deconstruct_keys(nil)` provides all available keys:
timestamp = Time.now.deconstruct_keys(nil)
# Usage in pattern-matching:
case timestamp
in year: ...2022
puts "Last year!"
in year: 2022, month: 1..3
puts "Last year's first quarter"
in year: 2023, month:, day:
puts "#{day} of #{month}th month!"
end
Standard Date
and DateTime
classes also have similar implementations for key deconstruction:
require 'date'
Date.today.deconstruct_keys(nil)
#=> {:year=>2023, :month=>1, :day=>15, :yday=>15, :wday=>0}
DateTime.now.deconstruct_keys(nil)
# => {:year=>2023, :month=>1, :day=>15, :yday=>15, :wday=>0, :hour=>17, :min=>19, :sec=>15, :sec_fraction=>(478525469/500000000), :zone=>"+02:00"}
Pattern matching with MatchData
result deconstruction:
case db_connection_string.match(%r{postgres://(\w+):(\w+)@(.+)})
in 'admin', password, server
# do connection with admin rights
in 'devuser', _, 'dev-server.us-east-1.rds.amazonaws.com'
# connect to dev server
in user, password, server
# do regular connection
end
YJIT – Yet Another Ruby JIT
YJIT, a lightweight, minimalistic Ruby JIT compiler built inside CRuby, is now an official part of the Ruby 3.2 runtime. It provides significantly higher performance, but also uses more memory than the Ruby interpreter and is generally suited for Ruby on Rails workloads.
By default, YJIT is not enabled in the Lambda Ruby 3.2 runtime. You can enable it for specific functions by setting the RUBY_YJIT_ENABLE
environment variable to 1
. Once enabled, you can verify it by printing the result of the RubyVM::YJIT.enabled?
method.
puts(RubyVM::YJIT.enabled?())
# => true
Using Ruby 3.2 in Lambda
AWS Cloud Development Kit (AWS CDK):
In AWS CDK, set the runtime attribute to Runtime.RUBY_3_2
when creating the function to use this version. In TypeScript:
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class MyCdkStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new lambda.Function(this, 'Ruby32Lambda', {
runtime: lambda.Runtime.RUBY_3_2, //execution environment
handler: 'test.handler', //file is “test”, function is “handler”
code: lambda.Code.fromAsset('lambda'), //code loaded from “lambda” dir
});
}
AWS Management Console
In the Lambda console, specify a runtime parameter value of Ruby 3.2 when creating or updating a function. The Ruby 3.2 runtime is now available in the Runtime dropdown in the Create function page.
To update an existing Lambda function to Ruby 3.2, navigate to the function in the Lambda console, then choose Edit in the Runtime settings panel. The new version of Ruby is available in the Runtime dropdown:
AWS Serverless Application Model
In the AWS Serverless Application Model (AWS SAM), set the Runtime attribute to ruby3.2
to use this version in your application deployments:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AWS Lambda Ruby 3.2 example
Resources:
Ruby32Lambda:
Type: AWS::Serverless::Function
Description: 'Lambda function that uses the Ruby3.2 runtime'
Properties:
FunctionName: Ruby32Lambda
Handler: function.handler
Runtime: ruby3.2
CodeUri: src/
Conclusion
Get started building with Ruby 3.2 today by making necessary changes for compatibility with Ruby 3.2, and specifying a runtime parameter value of ruby3.2 when creating or updating your Lambda functions. You can read about the Ruby programming model in the Lambda documentation to learn more about writing functions in Ruby 3.2.
For more serverless learning resources, visit Serverless Land.