From 37ca38df3d1a957846d5b50937ffbc6b4819f969 Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Mon, 10 Oct 2016 21:14:14 +0200 Subject: [PATCH] Expose `ctx._now` in update scripts (#20835) Update scripts might want to update the documents `_timestamp` but need a notion of `now()`. Painless doesn't support any notion of now() since it would make scripts non-pure functions. Yet, in the update case this is a valid value and we can pass it with the context together to allow the script to record the timestamp the document was updated. Relates to #17895 --- .../action/bulk/TransportShardBulkAction.java | 2 +- .../action/update/TransportUpdateAction.java | 2 +- .../action/update/UpdateHelper.java | 9 +- .../action/update/UpdateRequestTests.java | 83 ++++++++++++++++++- .../test/plan_a/25_script_upsert.yaml | 20 +++++ 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java b/core/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java index 7513d8889ec..c1303c6b56c 100644 --- a/core/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java +++ b/core/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java @@ -402,7 +402,7 @@ public class TransportShardBulkAction extends TransportWriteAction nowInMillis); Streamable action = result.action(); assertThat(action, instanceOf(IndexRequest.class)); IndexRequest indexAction = (IndexRequest) action; @@ -203,7 +216,7 @@ public class UpdateRequestTests extends ESTestCase { // We simulate that the document is not existing yet getResult = new GetResult("test", "type1", "2", 0, false, null, null); - result = updateHelper.prepare(new ShardId("test", "_na_", 0), updateRequest, getResult); + result = updateHelper.prepare(new ShardId("test", "_na_", 0), updateRequest, getResult, () -> nowInMillis); action = result.action(); assertThat(action, instanceOf(IndexRequest.class)); indexAction = (IndexRequest) action; @@ -276,4 +289,70 @@ public class UpdateRequestTests extends ESTestCase { assertThat(request.fetchSource().includes()[0], equalTo("path.inner.*")); assertThat(request.fetchSource().excludes()[0], equalTo("another.inner.*")); } + + public void testNowInScript() throws IOException { + Path genericConfigFolder = createTempDir(); + Settings baseSettings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) + .put(Environment.PATH_CONF_SETTING.getKey(), genericConfigFolder) + .build(); + Environment environment = new Environment(baseSettings); + Map, Object>> scripts = new HashMap<>(); + scripts.put("ctx._source.update_timestamp = ctx._now", + (vars) -> { + Map ctx = (Map) vars.get("ctx"); + Map source = (Map) ctx.get("_source"); + source.put("update_timestamp", ctx.get("_now")); + return null;}); + scripts.put("ctx._timestamp = ctx._now", + (vars) -> { + Map ctx = (Map) vars.get("ctx"); + ctx.put("_timestamp", ctx.get("_now")); + return null;}); + ScriptContextRegistry scriptContextRegistry = new ScriptContextRegistry(Collections.emptyList()); + ScriptEngineRegistry scriptEngineRegistry = new ScriptEngineRegistry(Collections.singletonList(new MockScriptEngine("mock", + scripts))); + + ScriptSettings scriptSettings = new ScriptSettings(scriptEngineRegistry, scriptContextRegistry); + ScriptService scriptService = new ScriptService(baseSettings, environment, + new ResourceWatcherService(baseSettings, null), scriptEngineRegistry, scriptContextRegistry, scriptSettings); + TimeValue providedTTLValue = TimeValue.parseTimeValue(randomTimeValue(), null, "ttl"); + Settings settings = settings(Version.CURRENT).build(); + + UpdateHelper updateHelper = new UpdateHelper(settings, scriptService); + + // We just upsert one document with now() using a script + IndexRequest indexRequest = new IndexRequest("test", "type1", "2") + .source(jsonBuilder().startObject().field("foo", "bar").endObject()) + .ttl(providedTTLValue); + + { + UpdateRequest updateRequest = new UpdateRequest("test", "type1", "2") + .upsert(indexRequest) + .script(new Script("ctx._source.update_timestamp = ctx._now", ScriptType.INLINE, "mock", Collections.emptyMap())) + .scriptedUpsert(true); + long nowInMillis = randomPositiveLong(); + // We simulate that the document is not existing yet + GetResult getResult = new GetResult("test", "type1", "2", 0, false, null, null); + UpdateHelper.Result result = updateHelper.prepare(new ShardId("test", "_na_", 0), updateRequest, getResult, () -> nowInMillis); + Streamable action = result.action(); + assertThat(action, instanceOf(IndexRequest.class)); + IndexRequest indexAction = (IndexRequest) action; + assertEquals(indexAction.sourceAsMap().get("update_timestamp"), nowInMillis); + } + { + UpdateRequest updateRequest = new UpdateRequest("test", "type1", "2") + .upsert(indexRequest) + .script(new Script("ctx._timestamp = ctx._now", ScriptType.INLINE, "mock", Collections.emptyMap())) + .scriptedUpsert(true); + long nowInMillis = randomPositiveLong(); + // We simulate that the document is not existing yet + GetResult getResult = new GetResult("test", "type1", "2", 0, true, new BytesArray("{}"), null); + UpdateHelper.Result result = updateHelper.prepare(new ShardId("test", "_na_", 0), updateRequest, getResult, () -> nowInMillis); + Streamable action = result.action(); + assertThat(action, instanceOf(IndexRequest.class)); + IndexRequest indexAction = (IndexRequest) action; + assertEquals(indexAction.timestamp(), Long.toString(nowInMillis)); + } + } } diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml b/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml index 2adf0de747f..3be567f2acb 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/plan_a/25_script_upsert.yaml @@ -63,4 +63,24 @@ - match: { _source.foo: xxx } + - do: + update: + index: test_1 + type: test + id: 3 + body: + script: + inline: "ctx._source.has_now = ctx._now > 0" + lang: "painless" + upsert: { has_now: false } + scripted_upsert: true + + - do: + get: + index: test_1 + type: test + id: 3 + + - match: { _source.has_now: true } +