From 74b8d81f72dd19397ea6b595bfb824903dd0a713 Mon Sep 17 00:00:00 2001 From: Brian Harrington Date: Wed, 13 Dec 2023 10:28:34 -0600 Subject: [PATCH] webapi: support unstable ops for expr manipulation Updates the `/expr/*` APIs to support unstable operations. These are used for debugging and understaning expressions so failing for unstable operations just adds confusion and makes it harder for early adopters to experiment with unstable features. --- .../atlas/core/stacklang/Interpreter.scala | 24 +++++++----- .../core/stacklang/StandardVocabulary.scala | 9 +++-- .../core/stacklang/InterpreterSuite.scala | 38 ++++++++++--------- .../com/netflix/atlas/webapi/ExprApi.scala | 9 +++-- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/Interpreter.scala b/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/Interpreter.scala index cc0f2fc00..8098a3066 100644 --- a/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/Interpreter.scala +++ b/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/Interpreter.scala @@ -113,21 +113,25 @@ case class Interpreter(vocabulary: List[Word]) { if (s.program.isEmpty) s.context else execute(nextStep(s)) } - final def execute(program: List[Any], context: Context, unfreeze: Boolean = true): Context = { + final def executeProgram( + program: List[Any], + context: Context, + unfreeze: Boolean = true + ): Context = { val result = execute(Step(program, context.incrementCallDepth)).decrementCallDepth if (unfreeze) result.unfreeze else result } - final def execute(program: List[Any]): Context = { - execute(program, Context(this, Nil, Map.empty)) + final def executeProgram(program: List[Any]): Context = { + executeProgram(program, Context(this, Nil, Map.empty)) } - final def execute(program: String): Context = { - execute(splitAndTrim(program)) - } - - final def execute(program: String, vars: Map[String, Any], features: Features): Context = { - execute(splitAndTrim(program), Context(this, Nil, vars, vars, features = features)) + final def execute( + program: String, + vars: Map[String, Any] = Map.empty, + features: Features = Features.STABLE + ): Context = { + executeProgram(splitAndTrim(program), Context(this, Nil, vars, vars, features = features)) } @scala.annotation.tailrec @@ -145,7 +149,7 @@ case class Interpreter(vocabulary: List[Word]) { } final def debug(program: List[Any]): List[Step] = { - debug(program, Context(this, Nil, Map.empty)) + debug(program, Context(this, Nil, Map.empty, features = Features.UNSTABLE)) } final def debug(program: String): List[Step] = { diff --git a/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/StandardVocabulary.scala b/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/StandardVocabulary.scala index 05b07106b..12918340b 100644 --- a/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/StandardVocabulary.scala +++ b/atlas-core/src/main/scala/com/netflix/atlas/core/stacklang/StandardVocabulary.scala @@ -57,7 +57,7 @@ object StandardVocabulary extends Vocabulary { override def matches(stack: List[Any]): Boolean = true override def execute(context: Context): Context = { - context.interpreter.execute(body, context, unfreeze = false) + context.interpreter.executeProgram(body, context, unfreeze = false) } override def summary: String = @@ -81,7 +81,7 @@ object StandardVocabulary extends Vocabulary { override def execute(context: Context): Context = { context.stack match { case (vs: List[?]) :: stack => - context.interpreter.execute(vs, context.copy(stack = stack), unfreeze = false) + context.interpreter.executeProgram(vs, context.copy(stack = stack), unfreeze = false) case _ => invalidStack } } @@ -198,7 +198,7 @@ object StandardVocabulary extends Vocabulary { context.stack match { case (f: List[?]) :: (vs: List[?]) :: stack => vs.reverse.foldLeft(context.copy(stack = stack)) { (c, v) => - c.interpreter.execute(f, c.copy(stack = v :: c.stack), unfreeze = false) + c.interpreter.executeProgram(f, c.copy(stack = v :: c.stack), unfreeze = false) } case _ => invalidStack } @@ -362,7 +362,8 @@ object StandardVocabulary extends Vocabulary { val init = context.copy(stack = stack) val res = vs.foldLeft(List.empty[Any] -> init) { case ((rs, c), v) => - val rc = c.interpreter.execute(f, c.copy(stack = v :: c.stack), unfreeze = false) + val rc = + c.interpreter.executeProgram(f, c.copy(stack = v :: c.stack), unfreeze = false) (rc.stack.head :: rs) -> rc.copy(stack = rc.stack.tail) } res._2.copy(stack = res._1.reverse :: res._2.stack) diff --git a/atlas-core/src/test/scala/com/netflix/atlas/core/stacklang/InterpreterSuite.scala b/atlas-core/src/test/scala/com/netflix/atlas/core/stacklang/InterpreterSuite.scala index 1ee739e48..38701662b 100644 --- a/atlas-core/src/test/scala/com/netflix/atlas/core/stacklang/InterpreterSuite.scala +++ b/atlas-core/src/test/scala/com/netflix/atlas/core/stacklang/InterpreterSuite.scala @@ -15,6 +15,7 @@ */ package com.netflix.atlas.core.stacklang +import com.netflix.atlas.core.util.Features import munit.FunSuite class InterpreterSuite extends FunSuite { @@ -40,28 +41,28 @@ class InterpreterSuite extends FunSuite { } test("empty") { - assertEquals(interpreter.execute(Nil), context(Nil)) + assertEquals(interpreter.executeProgram(Nil), context(Nil)) } test("push items") { - assertEquals(interpreter.execute(List("foo", "bar")), context(List("bar", "foo"))) + assertEquals(interpreter.executeProgram(List("foo", "bar")), context(List("bar", "foo"))) } test("execute word") { - assertEquals(interpreter.execute(List(":push-foo")), context(List("foo"))) + assertEquals(interpreter.executeProgram(List(":push-foo")), context(List("foo"))) } test("overloaded word") { - assertEquals(interpreter.execute(List(":overloaded")), context(List("one"))) + assertEquals(interpreter.executeProgram(List(":overloaded")), context(List("one"))) } test("overloaded word and some don't match") { - assertEquals(interpreter.execute(List(":overloaded2")), context(List("two"))) + assertEquals(interpreter.executeProgram(List(":overloaded2")), context(List("two"))) } test("word with no matches") { val e = intercept[IllegalStateException] { - interpreter.execute(List(":no-match")) + interpreter.executeProgram(List(":no-match")) } val expected = "no matches for word ':no-match' with stack [], candidates: [exception]" assertEquals(e.getMessage, expected) @@ -69,7 +70,7 @@ class InterpreterSuite extends FunSuite { test("using unstable word fails by default") { val e = intercept[IllegalStateException] { - interpreter.execute(List(":unstable")) + interpreter.executeProgram(List(":unstable")) } val expected = "to use :unstable enable unstable features" assertEquals(e.getMessage, expected) @@ -77,53 +78,56 @@ class InterpreterSuite extends FunSuite { test("unknown word") { val e = intercept[IllegalStateException] { - interpreter.execute(List("foo", ":unknown")) + interpreter.executeProgram(List("foo", ":unknown")) } assertEquals(e.getMessage, "unknown word ':unknown'") } test("unmatched closing paren") { val e = intercept[IllegalStateException] { - interpreter.execute(List(")")) + interpreter.executeProgram(List(")")) } assertEquals(e.getMessage, "unmatched closing parenthesis") } test("unmatched closing paren 2") { val e = intercept[IllegalStateException] { - interpreter.execute(List("(", ")", ")")) + interpreter.executeProgram(List("(", ")", ")")) } assertEquals(e.getMessage, "unmatched closing parenthesis") } test("unmatched opening paren") { val e = intercept[IllegalStateException] { - interpreter.execute(List("(")) + interpreter.executeProgram(List("(")) } assertEquals(e.getMessage, "unmatched opening parenthesis") } test("list") { val list = List("(", "1", ")") - assertEquals(interpreter.execute(list), context(List(List("1")))) + assertEquals(interpreter.executeProgram(list), context(List(List("1")))) } test("nested list") { val list = List("(", "1", "(", ")", ")") - assertEquals(interpreter.execute(list), context(List(List("1", "(", ")")))) + assertEquals(interpreter.executeProgram(list), context(List(List("1", "(", ")")))) } test("multiple lists") { val list = List("(", "1", ")", "(", "2", ")") - assertEquals(interpreter.execute(list), context(List(List("2"), List("1")))) + assertEquals(interpreter.executeProgram(list), context(List(List("2"), List("1")))) } test("debug") { + def createContext(stack: List[Any]): Context = { + Context(interpreter, stack, Map.empty, features = Features.UNSTABLE) + } val list = List("(", "1", ")", "(", "2", ")") val expected = List( - Interpreter.Step(list, Context(interpreter, Nil, Map.empty)), - Interpreter.Step(list.drop(3), Context(interpreter, List(List("1")), Map.empty)), - Interpreter.Step(Nil, Context(interpreter, List(List("2"), List("1")), Map.empty)) + Interpreter.Step(list, createContext(Nil)), + Interpreter.Step(list.drop(3), createContext(List(List("1")))), + Interpreter.Step(Nil, createContext(List(List("2"), List("1")))) ) assertEquals(interpreter.debug(list), expected) } diff --git a/atlas-webapi/src/main/scala/com/netflix/atlas/webapi/ExprApi.scala b/atlas-webapi/src/main/scala/com/netflix/atlas/webapi/ExprApi.scala index 2d8733050..554089797 100644 --- a/atlas-webapi/src/main/scala/com/netflix/atlas/webapi/ExprApi.scala +++ b/atlas-webapi/src/main/scala/com/netflix/atlas/webapi/ExprApi.scala @@ -30,6 +30,7 @@ import com.netflix.atlas.core.model.TimeSeriesExpr import com.netflix.atlas.core.stacklang.Context import com.netflix.atlas.core.stacklang.Interpreter import com.netflix.atlas.core.stacklang.Word +import com.netflix.atlas.core.util.Features import com.netflix.atlas.core.util.Strings import com.netflix.atlas.json.Json import com.netflix.atlas.pekko.CustomDirectives.* @@ -154,7 +155,7 @@ class ExprApi extends WebApi { // macros it alwasy returns true. This ensures the operation will actually be successful before // returning to a user. private def execWorks(interpreter: Interpreter, w: Word, ctxt: Context): Boolean = { - Try(interpreter.execute(List(s":${w.name}"), ctxt)).isSuccess + Try(interpreter.executeProgram(List(s":${w.name}"), ctxt)).isSuccess } private def matches(interpreter: Interpreter, w: Word, ctxt: Context): Boolean = { @@ -163,7 +164,7 @@ class ExprApi extends WebApi { private def processCompleteRequest(query: String, vocabName: String): HttpResponse = { val interpreter = newInterpreter(vocabName) - val result = interpreter.execute(query) + val result = interpreter.execute(query, features = Features.UNSTABLE) val candidates = interpreter.vocabulary.filter { w => matches(interpreter, w, result) @@ -185,7 +186,7 @@ class ExprApi extends WebApi { */ private def processQueriesRequest(expr: String, vocabName: String): HttpResponse = { val interpreter = newInterpreter(vocabName) - val result = interpreter.execute(expr) + val result = interpreter.execute(expr, features = Features.UNSTABLE) val exprs = result.stack.collect { case ModelExtractors.PresentationType(t) => t @@ -291,7 +292,7 @@ object ExprApi { } private def eval(interpreter: Interpreter, expr: String): List[StyleExpr] = { - interpreter.execute(expr).stack.collect { + interpreter.execute(expr, features = Features.UNSTABLE).stack.collect { case ModelExtractors.PresentationType(t) => t } }