Skip to content

Conversation

@ararslan
Copy link
Member

@ararslan ararslan commented Jul 3, 2024

Currently, @service defines a module every time it's called. That works fine but the downside is that you get module redefinition warnings if you happen to call @service again. The most plausible reason why one would want to redefine the module would be to enable/disable particular features in some region of code. To facilitate that use case while avoiding redefinition warnings, we can make @service check for an existing module, define one if it can't find one, and if it can then modify the feature set associated with the module in place. With this change, @service S3 (e.g.) effectively expands to:

if isdefined(Main, :S3)
    AWS.set_features!(S3.SERVICE_FEATURE_SET)
else
    module S3
        # contents
    end
end

The set_features! call uses the features specified in the macro call as keyword arguments. No arguments uses defaults. Notably this design requires that FeatureSet be made mutable, which seems fine IMO.


Copy link
Member

@quinnj quinnj left a comment

Choose a reason for hiding this comment

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

LGTM

@omus
Copy link
Member

omus commented Jul 4, 2024

Do you have a scenario in which you want to call @service multiple times for the same AWS service? Doing this within a package seems like an anti-pattern and I would be okay with only allowing a single call to @service S3 within a given module (the user must choose the feature flags they want and be consistent within that module).

The only reason I can think of to support multiple @service calls for the same AWS service would be on the REPL where you may want to experiment with different feature flags without restarting your Julia session.

@ararslan
Copy link
Member Author

ararslan commented Jul 8, 2024

Doing this within a package seems like an anti-pattern

I agree that the case this facilitates, dynamically changing the features in use for a given service, seems like an anti-pattern within a package. I'd be surprised if anybody would actually do that. The non-interactive use case I could imagine would be for folks who like to include all of their dependency loading at the top of each file in a package that utilizes those dependencies. (Not particularly common but I have seen it.)

The only reason I can think of to support multiple @service calls for the same AWS service would be on the REPL where you may want to experiment with different feature flags without restarting your Julia session.

I would consider this the primary use case.

@omus
Copy link
Member

omus commented Jun 12, 2025

I feel more comfortable with these changes after working on #720. I think I'll merge #720 first as that PR adds more extensive tests for @service

Currently, `@service` defines a module every time it's called. That
works fine but the downside is that you get module redefinition warnings
if you happen to call `@service` again. The most plausible reason why
one would want to redefine the module would be to enable/disable
particular features in some region of code. To facilitate that use case
while avoiding redefinition warnings, we can `@service` check for an
existing module, define one if it can't find one, and if it can then
modify the feature set associated with the module in place. With this
change, `@service S3` (e.g.) effectively expands to:

```julia
if isdefined(Main, :S3)
    AWS.set_features!(S3.SERVICE_FEATURE_SET)
else
    module S3
        # contents
    end
end
```

The `set_features!` call uses the features specified in the macro call
as keyword arguments. No arguments uses defaults. Notably this design
requires that `FeatureSet` be made mutable, which seems fine IMO.
@ararslan ararslan force-pushed the aa/no-replace-module branch from 6c78462 to 3e63626 Compare June 13, 2025 18:33
@omus
Copy link
Member

omus commented Jun 13, 2025

Thanks for updating the PR. I'll do some local experimentation with this yet. I expect this will be merged by or on Monday

Copy link
Member

@omus omus left a comment

Choose a reason for hiding this comment

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

I'm fine with this for supporting interactive usage but for package development modifying the global feature set for the @service module is dangerous as this is effectively modifying a global which changes the behaviour of all function calls using the service module.

I'm not seeing what problem this is solving beyond hiding a warning which can now be alternatively solved with using the import as syntax: @service STS as A use_response_type=true; @service STS as B use_response_type=false.

I see two options to move forward:

  1. We document the limitations of using @service on the same module name.
  2. We update the examples in @service to show how to use the same service with different feature sets enabled.

test/unit/AWS.jl Outdated
@service S3 use_response_type = true
features = S3.SERVICE_FEATURE_SET
@service S3
@test features === S3.SERVICE_FEATURE_SET # ensures module wasn't replaced
Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised that the containing testset captures this test.

Copy link
Member Author

Choose a reason for hiding this comment

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

¯\_(ツ)_/¯

@ararslan
Copy link
Member Author

I'm fine with this for supporting interactive usage but for package development modifying the global feature set for the @service module is dangerous as this is effectively modifying a global which changes the behaviour of all function calls using the service module.

💯 this should only ever be used interactively. Perhaps some strong wording should be added to the documentation to that effect.

I'm not seeing what problem this is solving beyond hiding a warning which can now be alternatively solved with using the import as syntax: @service STS as A use_response_type=true; @service STS as B use_response_type=false.

The annoyance is needing to assign a different name during interactive use. The situation is somewhat analogous to how before Julia 1.12 you couldn't redefine a type at the REPL, so you needed to use a different name if you modified the structure at all. Admittedly that case is a bit different since it was an error rather than a warning, but I see no reason not to make things in this case friendlier for interactive use.

@omus
Copy link
Member

omus commented Jul 4, 2025

I'm fine with this for supporting interactive usage but for package development modifying the global feature set for the @service module is dangerous as this is effectively modifying a global which changes the behaviour of all function calls using the service module.

💯 this should only ever be used interactively. Perhaps some strong wording should be added to the documentation to that effect.

I'm not seeing what problem this is solving beyond hiding a warning which can now be alternatively solved with using the import as syntax: @service STS as A use_response_type=true; @service STS as B use_response_type=false.

The annoyance is needing to assign a different name during interactive use. The situation is somewhat analogous to how before Julia 1.12 you couldn't redefine a type at the REPL, so you needed to use a different name if you modified the structure at all. Admittedly that case is a bit different since it was an error rather than a warning, but I see no reason not to make things in this case friendlier for interactive use.

Thanks for the explanation. What do you think about this approach which should allow for nicer interactive usage but avoid package developers from running into issues with mutating a service definition:

  • We only allow mutating the feature set for services defined in the Main module. This would support interactive REPL usage
  • We disallow using @service to overwrite an existing binding when the module name is not Main. If users have defined say @service STS multiple times in their package they would now get an error. Alternately, we could allow the module to be overwritten still which would ensure the behaviour isn't breaking.

@omus
Copy link
Member

omus commented Jul 4, 2025

I implemented the only mutate on Main feature I mentioned so we can have a concrete discussion. Additionally, when looking over set_features! it seemed a little odd it would reset fields instead of just replacing what was passed in. I revised the code so that SERVICE_FEATURE_SET uses a Ref instead so that FeatureSet no longer needs to be mutable.

Let me know if this works for you and we should be able to move forward.

@ararslan
Copy link
Member Author

ararslan commented Jul 8, 2025

We only allow mutating the feature set for services defined in the Main module. This would support interactive REPL usage

The active module at the REPL isn't necessarily Main though. I guess we could condition on Base.active_module(), but that feels a little weird. Special-casing a module in general feels weird to me; do you know whether there's precedent for that in other packages? I'd favor documenting the intended use but not limiting the implementation.

I revised the code so that SERVICE_FEATURE_SET uses a Ref instead so that FeatureSet no longer needs to be mutable.

That seems fine to me

@omus
Copy link
Member

omus commented Jul 9, 2025

We only allow mutating the feature set for services defined in the Main module. This would support interactive REPL usage

The active module at the REPL isn't necessarily Main though. I guess we could condition on Base.active_module(), but that feels a little weird. Special-casing a module in general feels weird to me; do you know whether there's precedent for that in other packages? I'd favor documenting the intended use but not limiting the implementation.

I don't have any precedent for this. Thinking about this more we don't care about the calling module but rather if Julia is being run interactively or not. I think Base.interactive is probably a better solution.

@ararslan
Copy link
Member Author

ararslan commented Jul 9, 2025

Thinking about this more we don't care about the calling module but rather if Julia is being run interactively or not.

I could actually see a non-interactive use in tests, like say you want to run tests in a loop over different service settings. That seems like it would be difficult to achieve if you're only able to do @service whatever multiple times when running interactively.

@omus
Copy link
Member

omus commented Jul 9, 2025

Thinking about this more we don't care about the calling module but rather if Julia is being run interactively or not.

I could actually see a non-interactive use in tests, like say you want to run tests in a loop over different service settings. That seems like it would be difficult to achieve if you're only able to do @service whatever multiple times when running interactively.

For this scenario I think we should recommend this instead:

@service whatever as whatever1 use_response_type = false
@service whatever as whatever2 use_response_type = true

@ararslan
Copy link
Member Author

ararslan commented Jul 9, 2025

Right, but I'm imagining a loop over multiple configurations with tests run inside of that loop. At first my thought was that you would need to loop over the configurations themselves and call @service in the loop, but while writing this comment I realized that you can do your @service calls up front with appropriate renames and loop over the renamed service modules, which also avoids needing to use @eval in the loop. Okay, never mind the test scenario then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants