Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate lambdex for building FaaS packages (pending) #19032

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/python/pants/backend/awslambda/python/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pants.core.util_rules.environments import EnvironmentField
from pants.engine.rules import Get, collect_rules, rule
from pants.engine.unions import UnionRule
from pants.option.global_options import GlobalOptions
from pants.util.logging import LogLevel

logger = logging.getLogger(__name__)
Expand All @@ -44,9 +45,9 @@ class PythonAwsLambdaFieldSet(PackageFieldSet):

@rule(desc="Create Python AWS Lambda", level=LogLevel.DEBUG)
async def package_python_awslambda(
field_set: PythonAwsLambdaFieldSet,
field_set: PythonAwsLambdaFieldSet, global_options: GlobalOptions
) -> BuiltPackage:
layout = PythonFaaSLayout(field_set.layout.value)
layout = field_set.layout.resolve_value(field_set.address, global_options)

if layout is PythonFaaSLayout.LAMBDEX:
return await Get(
Expand Down
12 changes: 12 additions & 0 deletions src/python/pants/backend/awslambda/python/rules_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def complete_platform(rule_runner: PythonRuleRunner) -> bytes:
).stdout


_first_run_of_deprecated_warning = True


@pytest.mark.platform_specific_behavior
@pytest.mark.parametrize(
"major_minor_interpreter",
Expand Down Expand Up @@ -180,6 +183,15 @@ def handler(event, context):
if sys.platform == "darwin":
assert "`python_awslambda` targets built on macOS may fail to build." in caplog.text

global _first_run_of_deprecated_warning
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a big code smell, but also we don't typically test deprecation messages, as they are comprehensively tested generically. Manually observing that the right thing happens is sufficient.

if _first_run_of_deprecated_warning:
_first_run_of_deprecated_warning = False
assert (
'DEPRECATED: use of `layout="lambdex"` (set by default) in target src/python/foo/bar:lambda is scheduled to be removed'
in caplog.text
)
assert "use_deprecated_lambdex_layout" in caplog.text

zip_file_relpath, content = create_python_awslambda(
rule_runner,
Address("src/python/foo/bar", target_name="slimlambda"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pants.core.util_rules.environments import EnvironmentField
from pants.engine.rules import Get, collect_rules, rule
from pants.engine.unions import UnionRule
from pants.option.global_options import GlobalOptions
from pants.util.logging import LogLevel

logger = logging.getLogger(__name__)
Expand All @@ -44,9 +45,10 @@ class PythonGoogleCloudFunctionFieldSet(PackageFieldSet):

@rule(desc="Create Python Google Cloud Function", level=LogLevel.DEBUG)
async def package_python_google_cloud_function(
field_set: PythonGoogleCloudFunctionFieldSet,
field_set: PythonGoogleCloudFunctionFieldSet, global_options: GlobalOptions
) -> BuiltPackage:
layout = PythonFaaSLayout(field_set.layout.value)
layout = field_set.layout.resolve_value(field_set.address, global_options)

if layout is PythonFaaSLayout.LAMBDEX:
return await Get(
BuiltPackage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ def complete_platform(rule_runner: PythonRuleRunner) -> bytes:
).stdout


_first_run_of_deprecated_warning = True


@pytest.mark.platform_specific_behavior
@pytest.mark.parametrize(
"major_minor_interpreter",
Expand Down Expand Up @@ -180,6 +183,15 @@ def handler(event, context):
in caplog.text
)

global _first_run_of_deprecated_warning
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

if _first_run_of_deprecated_warning:
_first_run_of_deprecated_warning = False
assert (
'DEPRECATED: use of `layout="lambdex"` (set by default) in target src/python/foo/bar:lambda is scheduled to be removed'
in caplog.text
)
assert "use_deprecated_lambdex_layout" in caplog.text


def test_warn_files_targets(rule_runner: PythonRuleRunner, caplog) -> None:
rule_runner.write_files(
Expand Down
24 changes: 23 additions & 1 deletion src/python/pants/backend/python/subsystems/lambdex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@
from pants.backend.python.subsystems.python_tool_base import LockfileRules, PythonToolBase
from pants.backend.python.target_types import ConsoleScript
from pants.engine.rules import collect_rules
from pants.util.strutil import softwrap


class Lambdex(PythonToolBase):
# these aren't read automatically, but are defined for use in faas.py
removal_hint = softwrap(
"""
Either use `layout=\"zip\"` in `python_awslambda` or `python_google_cloud_function` targets
to build flat packages without dynamic PEX start-up (recommended), or use `pex_binary` if dependency
selection is required on start-up (for instance, one package is deployed to multiple
runtimes). For a `pex_binary`, add `__pex__` to the import path for the handler: for
example, if the handler function `func` is defined in `foo/bar.py`, configure
`__pex__.foo.bar.func` as the handler.
"""
)
removal_version = "2.19.0.dev0"

options_scope = "lambdex"
help = "A tool for turning .pex files into Function-as-a-Service artifacts (https://github.com/pantsbuild/lambdex)."
help = softwrap(
f"""
A tool for turning .pex files into Function-as-a-Service artifacts (https://github.com/pantsbuild/lambdex).
Lambdex is no longer necessary: {removal_hint}
This will be removed in Pants {removal_version}.
"""
)

default_version = "lambdex>=0.1.9"
default_main = ConsoleScript("lambdex")
Expand Down
46 changes: 40 additions & 6 deletions src/python/pants/backend/python/util_rules/faas.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from pants.backend.python.util_rules.pex_from_targets import rules as pex_from_targets_rules
from pants.backend.python.util_rules.pex_venv import PexVenv, PexVenvLayout, PexVenvRequest
from pants.backend.python.util_rules.pex_venv import rules as pex_venv_rules
from pants.base.deprecated import warn_or_error
from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact, OutputPathField
from pants.core.target_types import FileSourceField
from pants.engine.addresses import Address, UnparsedAddressInputs
Expand Down Expand Up @@ -72,10 +73,11 @@
targets_with_sources_types,
)
from pants.engine.unions import UnionMembership, UnionRule
from pants.option.global_options import GlobalOptions
from pants.source.filespec import Filespec
from pants.source.source_root import SourceRoot, SourceRootRequest
from pants.util.docutil import bin_name, doc_url
from pants.util.strutil import help_text
from pants.util.strutil import help_text, softwrap

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -284,23 +286,23 @@ def to_platform_string(self) -> None | str:


class PythonFaaSLayout(Enum):
# TODO: deprecate lambdex layout, since PEX can be used directly now
LAMBDEX = "lambdex"
ZIP = "zip"


class PythonFaaSLayoutField(StringField):
alias = "layout"
valid_choices = PythonFaaSLayout
default = PythonFaaSLayout.LAMBDEX.value
default = None

help = help_text(
"""
The layout used for the function artifact.
With the `lambdex` layout (default), the artifact is created as a Lambdex, which is a normal
PEX that's been adjusted to include a shim file for the handler. This requires dynamically
choosing dependencies on start-up.
With the `lambdex` layout (default unless `[GLOBAL].use_deprecated_lambdex_layout` is set to
`false`, deprecated), the artifact is created as a Lambdex, which is a normal PEX that's
been adjusted to include a shim file for the handler. This requires dynamically choosing
dependencies on start-up.
With the `zip` layout (recommended), the artifact contains first and third party code at the
top level, similar to building with `pip install --target=...`. This layout chooses the
Expand All @@ -311,6 +313,38 @@ class PythonFaaSLayoutField(StringField):
"""
)

# TODO: this whole function can disappear once the LAMBDEX option is removed
def resolve_value(self, address: Address, global_options: GlobalOptions) -> PythonFaaSLayout:
if self.value is None:
layout = (
PythonFaaSLayout.LAMBDEX
if global_options.use_deprecated_lambdex_layout
else PythonFaaSLayout.ZIP
)
else:
layout = PythonFaaSLayout(self.value)

if layout is PythonFaaSLayout.LAMBDEX and global_options.options.is_default(
"use_deprecated_lambdex_layout"
):
from_default = " (set by default)" if self.value is None else ""
warn_or_error(
Lambdex.removal_version,
f'use of `layout="lambdex"`{from_default} in target {address}',
softwrap(
f"""
{Lambdex.removal_hint}
Set `use_deprecated_lambdex_layout = false` in the `[GLOBAL]` section of
`pants.toml` to switch to using `layout="zip"` by default everywhere, or set it
to `true` to temporarily continue with the old behaviour and silence this
warning.
"""
),
)

return layout


@rule
async def digest_complete_platforms(
Expand Down
14 changes: 14 additions & 0 deletions src/python/pants/option/global_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,20 @@ class GlobalOptions(BootstrapOptions, Subsystem):
default=[],
)

use_deprecated_lambdex_layout = BoolOption(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a global option? In fact, why do we need this option at all?

I might be missing something, but I think it would be simpler to do the relevant check in the code that uses the subsystem, and issue the deprecation warning there?

I'm not sure why this is a complicated as it is, so maybe I'm not seeing something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to allow users (including my work repo) to opt in to the new layout="zip"-by-default behaviour early and globally. In particular, I wouldn't want to have to set it on each target when I upgrade to 2.18... and then go through and delete them all in 2.19, once the default flips. I also thought it would be nice to allow users to silence the warning, but that's less critical.

I suppose another option would be: unconditional (but memoized etc.) warning as it is, and users can add a __defaults__ in the root BUILD to reduces the updates required. This does mean pants won't be able prompt users to remove it when it's not necessary, but that's probably okay, I can have the warning suggest something with a comment.

I'm more than happy to be guided to simpler options, if you think I'm solving problems that don't need to be solved!

(Side note, is 2.19 the right release to remove? Maybe it should be a longer deprecation period?)

Copy link
Contributor

@benjyw benjyw May 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a very low chance that switching from lambdex to zip will have any negative consequence, so I think 2.19.x is fine!

But I'm sure we can find a much simpler way of deprecating. For one thing, maybe (and I wish I'd thought of this when reviewing that change, sorry) we don't need a per-target layout at all, and this can be a repo-wide setting? After all, we're about to get rid of it entirely, so the intermediate state of "some lambdas use lambdex and some use zip" seems unnecessary. Esp. since we expect zero user-noticeable difference except better performance (and I guess the name change of the handler). That change has not been in any release yet, so not too late to replace it.

Instead, we can put layout on the lambdex scope (for want of a better one), and default it to lambdex. But, if you rely on that default in 2.17.x (I think a cherry-pick is fine for this) you get a warning that you should set it explicitly and that lambdex is going away. Then we make zip the default in 2.18.x, with a warning if you set to lambdex (or if you set explicitly to zip, since that is now the default, and the option is going away), and then we remove the option entirely in 2.19.x.

Does that make sense? This is often how we do stuff like this - introduce an option to deprecate a feature, then deprecation the option one release later.

Either way, this shouldn't be on the global options, because it is very specific to a backend.

Copy link
Contributor Author

@huonw huonw May 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For one thing, maybe (and I wish I'd thought of this when reviewing that change, sorry) we don't need a per-target layout at all, and this can be a repo-wide setting?

Does that make sense? This is often how we do stuff like this - introduce an option to deprecate a feature, then deprecation the option one release later.

Yeah, that makes a lot of sense.

Simplifying layout to a global option is way better 👍 I had originally been thinking of other layouts that do make sense per-target (e.g. layout="directory" to not have it zipped, or even layout="layer"), but that's all a total distraction from the core of "move away from Lambdex" and should be handled independently.

Next steps for me:

  1. revert Implement layout="zip" for Lambda/GCF, skipping lambdex #19022, because it'll make the next steps cleaner: Revert "Implement layout="zip" for Lambda/GCF, skipping lambdex" #19071
  2. set-up the global layout = "lambdex" setting: Add [lambdex].layout to opt-in to soon-to-be-deprecated FaaS behaviour #19074
  3. redo Implement layout="zip" for Lambda/GCF, skipping lambdex #19022, getting rid of the layout field and using the global setting instead
  4. return to this PR if required

Thanks for taking the time to think through a better approach!

default=True,
help=softwrap(
"""
If `true`, any `python_awslambda` and `python_google_cloud_function` targets use the
`layout="lambdex"` by default. This is the older behaviour of these targets, and is
being removed.
If `false`, they will instead use the improved `layout="zip"` by default, which will
become the default in future.
"""
),
)

@classmethod
def validate_instance(cls, opts):
"""Validates an instance of global options for cases that are not prohibited via
Expand Down