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

Script: Metadata for update context #88333

Merged
merged 68 commits into from
Jul 19, 2022

Conversation

stu-elastic
Copy link
Contributor

@stu-elastic stu-elastic commented Jul 6, 2022

Adds the metadata() API call and a Metadata class for the Update context.

There are different metadata available in the update context depending
on whether it is an update or an insert (via upsert).

For update, scripts can read index, id, routing, version and timestamp.

For insert, scripts can read index, id and timestamp.

Scripts can always read and write the op but the available ops are different.

Updates allow 'noop', 'index' and 'delete'.
Inserts allow 'noop' and 'create'.

Refs: #86472

Adds the metadata() API call and a Metadata class for the Update context

There are different metadata available in the update context depending
on whether it is an update or an insert (via upsert).

For update, scripts can read index, id, routing, version and timestamp.

For insert, scripts can read index, id and timestamp.

Scripts can always read and write the op but the available ops are different.
Updates allow 'noop', 'index' and 'delete'.
Inserts allow 'noop' and 'create'.

Refs: elastic#86472
@stu-elastic stu-elastic added the :Core/Infra/Scripting Scripting abstractions, Painless, and Mustache label Jul 7, 2022
@stu-elastic stu-elastic marked this pull request as ready for review July 7, 2022 01:36
@elasticmachine elasticmachine added the Team:Core/Infra Meta label for core/infra team label Jul 7, 2022
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra (Team:Core/Infra)

@stu-elastic stu-elastic requested a review from jdconrad July 7, 2022 01:36
@elasticsearchmachine
Copy link
Collaborator

Hi @stu-elastic, I've created a changelog YAML for you.

@sethmlarson sethmlarson added the Team:Clients Meta label for clients team label Jul 7, 2022
@elasticmachine
Copy link
Collaborator

Pinging @elastic/clients-team (Team:Clients)

@stu-elastic stu-elastic requested a review from rjernst July 7, 2022 17:02
Copy link
Contributor

@jdconrad jdconrad left a comment

Choose a reason for hiding this comment

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

I realized we have talked about this, but after viewing the code I was wondering again what if each context did have its own Metadata and the base class for this took in the Map of validators? The numerous comments in the Metadata class about how something is needed only for one context, but not another seems like it'll be hard to maintain. We also wouldn't have to have methods on the class that don't make sense for a specific context. You still wouldn't have to alias the name because it's part of the allow list specific to each context.

I can see there's a lot of refactoring work as well as you mentioned to me. Since I'm just coming back into this project it's tricky for me to differentiate what's refactoring versus what's added here that's new. Would you be able to split the refactoring into a first PR please?

@stu-elastic
Copy link
Contributor Author

@rjernst and @jdconrad, I've changed this PR to use infrastructure from #88401 and #88458. Can you take another pass?

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

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

Thanks for the update Stu! After the other PRs this is much more straightforward and easier to understand. I think there is a backcompat issue to address, but otherwise I just have a few minor comments.

@SuppressWarnings("unchecked")
public Map<String, Object> getSource() {
Map<String, Object> wrapped = super.getSource();
Object rawSource = wrapped.get(SOURCE);
Copy link
Member

Choose a reason for hiding this comment

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

Since ctx._source seems to be the more common pattern (only ingest differs?), wouldn't it make more sense for IngestCtxMap to be the special case, and CtxMap has the implementation from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved.

@Override
public void setOp(String op) {
if (LEGACY_NOOP_STRING.equals(op)) {
throw new IllegalArgumentException(LEGACY_NOOP_STRING + " is deprecated, use 'noop' instead");
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem like a deprecation, an exception would mean an error. I think we need to support the existing case without error for now.

Copy link
Contributor Author

@stu-elastic stu-elastic Jul 18, 2022

Choose a reason for hiding this comment

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

It is supported in the map, so there's no bwc issue, what do you think about removing the " is deprecated" language, disallow it in set but translate it in getOp?

That's asymmetric, but the addition of this class seems like the time and place to make that change.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I see now. I think your suggestion is good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, keeping it.

String getId()
String getRouting()
long getVersion()
String getOp()
Copy link
Member

Choose a reason for hiding this comment

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

We discussed having get and set op outside of the metadata on each script type that needs it, as it isn't really metadata about the document. What happened with that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it's on the script, then it conflicts with any variables named op, so it would be a backwards compatibility issue.

After thinking about it some more, it seems like a high cost to introduce another concept in addition to field and metadata for this one-off. Maybe there's a better way to handle it?

Copy link
Member

Choose a reason for hiding this comment

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

I don't feel strongly about it being outside of metadata. The potential conflict with variable names is both unfortunate, and something I think we need to look at fixing (via shadowing), because it's not just with class getters, but also any additional arguments added to the execute method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The potential conflict with variable names is both unfortunate, and something I think we need to look at fixing (via shadowing), because it's not just with class getters, but also any additional arguments added to the execute method.

Agreed. It's a tricky change but worth doing because it allows us to make changes without backward compatibility issues. It's come up a few times now.

SET_ONCE_LONG
);

protected static BiConsumer<String, String> setValidator(Set<String> valid) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: the naming here makes it look like a setter. perhaps stringSetValidator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed.

@Override
public String getOp() {
String op = super.getOp();
if (LEGACY_NOOP_STRING.equals(op) || op == null) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can do this translation for the "legacy" value here, it would be breaking. We should look at how/when to deprecate the two noop values outside of this PR.

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 don't think it's breaking since the Map interface still allows it, it's just if the script wants to use the metadata interface. I can allow it here but still reject it in setOp(String), what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

The legacy case makes sense, but why is null converted to noop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

op is nullable for backwards compatibility.

Before this change, if op were null, lenientFromString would translate it to UpdateOp.NONE.

UpdateOp.NONE is the same as 'noop' now.

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 suppose we should also restrict null in setOp like we're already doing with "none".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact, there shouldn't be validation for op at all in the map, as it all gets translated to UpdateOp.NONE.


@Override
public String getRouting() {
throw new IllegalArgumentException("routing is unavailable for insert");
Copy link
Member

Choose a reason for hiding this comment

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

UnsupportedOperationException would be more typical. We should use the same anywhere else we are failing a get/set method (a user should never see it since we woulnd't have allowed the method on scripting to begin with).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to UnsupportedOperationException.

It is still whitelisted because an Update script may run in "Update mode" or "Insert-via-upsert mode" and this allows a user to write a script that will work in either case. If we split the Update context then a script that works for "Update mode" would fail to compile for "Insert-via-upsert mode".

Copy link
Member

Choose a reason for hiding this comment

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

If we split the Update context then a script

I thought we were already planning to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We discussed it a while ago, but it'd be weird to have an script sent to the update endpoint fail to compile based on the whether or not the document exists.

if (rawSource instanceof Map<?, ?> map) {
return (Map<String, Object>) map;
}
throw new IllegalArgumentException(
Copy link
Member

Choose a reason for hiding this comment

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

How could this occur? Since we control the ctx map, wouldn't this be a coding error? Should this be an assert instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

An assert brings down the node. Do you think we'd get the same feedback with an Exception without the customer pain? Perhaps IllegalStateException.

Copy link
Member

@rjernst rjernst Jul 18, 2022

Choose a reason for hiding this comment

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

An assert brings down the node

It won't for a couple reasons. The uncaught exception handler does not consider AssertionError to be fatal. But also it would only be tripped if running with asserts enabled, which doesn't happen in production. We run tests with asserts enabled, and use asserts for things like this in many other parts of the code specifically so that we can catch coding errors in tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to assert

Copy link
Contributor

@jdconrad jdconrad left a comment

Choose a reason for hiding this comment

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

The new changes look fine to me. Please wait for @rjernst to have another look before committing.

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

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

LGTM.

@stu-elastic stu-elastic merged commit fa25c31 into elastic:master Jul 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:Core/Infra/Scripting Scripting abstractions, Painless, and Mustache >enhancement Team:Clients Meta label for clients team Team:Core/Infra Meta label for core/infra team v8.4.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants