You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by ri...@apache.org on 2021/10/01 13:30:43 UTC

[geode] branch develop updated: GEODE-9650: LOLWUT command (#6908)

This is an automated email from the ASF dual-hosted git repository.

ringles pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 9c597af  GEODE-9650: LOLWUT command (#6908)
9c597af is described below

commit 9c597afabe8eec1350feff2e06c29c2d7a192e75
Author: Ray Ingles <ri...@pivotal.io>
AuthorDate: Fri Oct 1 09:29:30 2021 -0400

    GEODE-9650: LOLWUT command (#6908)
    
    This implements a version of the Redis LOLWUT command, which is supposed to (a) algorithmically generate computer art, and (b) output the program version. Optionally, the LOLWUT command can take arguments to affect the output.
    
    This implementation generates arbitrary-size mazes. By default, it generates a 40-cell-wide maze of height 10, but given arguments it can generate a valid maze of any width and height (up to 1Kx1M), while consuming a small and fixed amount of memory. Below the maze, it outputs the Geode version.
    
    Co-authored-by: Ray Ingles <ri...@vmware.com>
---
 .../executor/server/LolWutIntegrationTest.java     | 139 ++++++++++++++++
 .../geode/redis/internal/RedisCommandType.java     |   3 +
 .../internal/executor/server/LolWutExecutor.java   | 180 +++++++++++++++++++++
 .../redis/internal/netty/StringBytesGlossary.java  |   4 +
 4 files changed, 326 insertions(+)

diff --git a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/LolWutIntegrationTest.java b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/LolWutIntegrationTest.java
new file mode 100644
index 0000000..ff6ae46
--- /dev/null
+++ b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/LolWutIntegrationTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.redis.internal.executor.server;
+
+import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.util.SafeEncoder;
+
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.redis.GeodeRedisServerRule;
+import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LolWutIntegrationTest implements RedisIntegrationTest {
+  private static final int REDIS_CLIENT_TIMEOUT = RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+  private static final String GEODE_VERSION_STRING = KnownVersion.getCurrentVersion().toString();
+  private static final int DEFAULT_MAZE_HEIGHT = 10 + 2; // Top & bottom walls, version string
+  private static final int MAX_MAZE_HEIGHT = (1024 * 1024) + 2;
+  private Jedis jedis;
+
+  @ClassRule
+  public static GeodeRedisServerRule server = new GeodeRedisServerRule()
+      .withProperty(LOG_LEVEL, "info");
+
+  @Override
+  public int getPort() {
+    return server.getPort();
+  }
+
+  @Before
+  public void setUp() {
+    jedis = new Jedis("localhost", getPort(), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    jedis.flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void shouldReturnGeodeVersion() {
+    String actualResult =
+        Coder.bytesToString((byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut")));
+
+    assertThat(actualResult).contains(GEODE_VERSION_STRING);
+  }
+
+  @Test
+  public void shouldReturnDefaultMazeSize_givenNoArgs() {
+    String actualResult =
+        Coder.bytesToString((byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut")));
+
+    String[] lines = actualResult.split("\\n");
+    assertThat(lines.length).isEqualTo(DEFAULT_MAZE_HEIGHT);
+  }
+
+
+  @Test
+  public void shouldReturnSpecifiedMazeWidth_givenNumericArg() {
+    String actualResult = Coder.bytesToString(
+        (byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut"),
+            SafeEncoder.encode("20")));
+
+    String[] lines = actualResult.split("\\n");
+    assertThat(lines[0].length()).isEqualTo((20 - 1) * 2);
+  }
+
+  @Test
+  public void shouldReturnSpecifiedMazeHeight_givenNumericArgs() {
+    String actualResult = Coder.bytesToString(
+        (byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut"),
+            SafeEncoder.encode("10"),
+            SafeEncoder.encode("20")));
+
+    String[] lines = actualResult.split("\\n");
+    assertThat(lines.length).isEqualTo(20 + 2);
+  }
+
+  @Test
+  public void shouldLimitMazeWidth() {
+    String actualResult = Coder.bytesToString(
+        (byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut"),
+            SafeEncoder.encode("2048")));
+
+    String[] lines = actualResult.split("\\n");
+    assertThat(lines[0].length()).isEqualTo((1024 - 1) * 2);
+  }
+
+  @Test
+  public void shouldLimitMazeHeight() {
+    String actualResult = Coder.bytesToString(
+        (byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut"),
+            SafeEncoder.encode("10"),
+            SafeEncoder.encode(Integer.toString(MAX_MAZE_HEIGHT * 2))));
+
+    String[] lines = actualResult.split("\\n");
+    assertThat(lines.length).isEqualTo(MAX_MAZE_HEIGHT);
+  }
+
+  @Test
+  public void shouldNotError_givenVersionArg() {
+    String actualResult = Coder.bytesToString(
+        (byte[]) jedis.sendCommand(() -> SafeEncoder.encode("lolwut"),
+            SafeEncoder.encode("version"),
+            SafeEncoder.encode("ignored")));
+
+    assertThat(actualResult).contains(GEODE_VERSION_STRING);
+  }
+
+  @Test
+  public void shouldError_givenNonNumericArg() {
+    assertThatThrownBy(() -> jedis.sendCommand(() -> SafeEncoder.encode("lolwut"),
+        SafeEncoder.encode("notEvenCloseToANumber")))
+            .hasMessage("ERR value is not an integer or out of range");
+  }
+
+}
diff --git a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
index 9a82755..c2d0b2c 100755
--- a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
+++ b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
@@ -85,6 +85,7 @@ import org.apache.geode.redis.internal.executor.server.CommandExecutor;
 import org.apache.geode.redis.internal.executor.server.DBSizeExecutor;
 import org.apache.geode.redis.internal.executor.server.FlushAllExecutor;
 import org.apache.geode.redis.internal.executor.server.InfoExecutor;
+import org.apache.geode.redis.internal.executor.server.LolWutExecutor;
 import org.apache.geode.redis.internal.executor.server.ShutDownExecutor;
 import org.apache.geode.redis.internal.executor.server.SlowlogExecutor;
 import org.apache.geode.redis.internal.executor.server.TimeExecutor;
@@ -283,6 +284,8 @@ public enum RedisCommandType {
       .flags(ADMIN, RANDOM, LOADING, STALE)),
   INFO(new InfoExecutor(), SUPPORTED, new Parameter().min(1).max(2, ERROR_SYNTAX).firstKey(0)
       .flags(RANDOM, LOADING, STALE)),
+  LOLWUT(new LolWutExecutor(), SUPPORTED, new Parameter().min(1).firstKey(0).flags(READONLY, FAST)),
+
 
   /********** Publish Subscribe **********/
   SUBSCRIBE(new SubscribeExecutor(), SUPPORTED, new Parameter().min(2).firstKey(0).flags(
diff --git a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/server/LolWutExecutor.java b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/server/LolWutExecutor.java
new file mode 100755
index 0000000..cc2f662
--- /dev/null
+++ b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/server/LolWutExecutor.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ *
+ */
+package org.apache.geode.redis.internal.executor.server;
+
+
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
+import static org.apache.geode.redis.internal.netty.Coder.equalsIgnoreCaseBytes;
+import static org.apache.geode.redis.internal.netty.StringBytesGlossary.bVERSION;
+
+import java.util.List;
+import java.util.Random;
+
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.redis.internal.executor.AbstractExecutor;
+import org.apache.geode.redis.internal.executor.RedisResponse;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.Command;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LolWutExecutor extends AbstractExecutor {
+
+  public static final int MAX_MAZE_WIDTH = 1024; // limit width to keep memory usage low
+  public static final int MAX_MAZE_HEIGHT = 1024 * 1024; // if user wants a bigger maze they can
+                                                         // recompile
+  public static final int DEFAULT_WIDTH = 40;
+  public static final int DEFAULT_HEIGHT = 10;
+  private static int width;
+  private static int height;
+
+  @Override
+  public RedisResponse executeCommand(Command command,
+      ExecutionHandlerContext context) {
+
+    width = DEFAULT_WIDTH;
+    height = DEFAULT_HEIGHT;
+    int inputWidth = -1;
+    int inputHeight = -1;
+
+    List<byte[]> commands = command.getProcessedCommand();
+    if (commands.size() > 1) {
+      for (int i = 1; i < commands.size(); i++) {
+        if (equalsIgnoreCaseBytes(commands.get(i), bVERSION)) {
+          i += 1; // skip next arg, we only have one version for now
+        } else {
+          try {
+            if (inputWidth < 0) {
+              inputWidth = Coder.narrowLongToInt(Coder.bytesToLong(commands.get(i)));
+              if (inputWidth > MAX_MAZE_WIDTH) {
+                inputWidth = MAX_MAZE_WIDTH;
+              }
+            } else if (inputHeight < 0) {
+              inputHeight = Coder.narrowLongToInt(Coder.bytesToLong(commands.get(i)));
+              if (inputHeight > MAX_MAZE_HEIGHT) {
+                inputHeight = MAX_MAZE_HEIGHT;
+              }
+            } else {
+              break; // all required args filled
+            }
+          } catch (NumberFormatException ignored) {
+            return RedisResponse.error(ERROR_NOT_INTEGER);
+          }
+        }
+      }
+    }
+    if (inputHeight >= 0) {
+      height = inputHeight;
+    }
+    if (inputWidth >= 0) {
+      width = inputWidth;
+    }
+
+    return RedisResponse.bulkString(makeArbitrarySizeMaze());
+  }
+
+  // Adapted from code here: https://tromp.github.io/maze.html
+  private String makeArbitrarySizeMaze() {
+    StringBuilder mazeString = new StringBuilder();
+    int[] leftLinks = new int[width];
+    int[] rightLinks = new int[width];
+
+    Random rand = new Random();
+    leftLinks[0] = 1;
+
+    mazeTopAndEntrance(mazeString, leftLinks, rightLinks);
+
+    mazeRows(mazeString, leftLinks, rightLinks, rand);
+
+    mazeBottomRowAndExit(mazeString, leftLinks, rightLinks, rand);
+
+    mazeString.append("\n " + KnownVersion.getCurrentVersion().toString() + "\n");
+
+    return mazeString.toString();
+  }
+
+  private void mazeTopAndEntrance(StringBuilder mazeString, int[] leftLinks,
+      int[] rightLinks) {
+    int tempIndex;
+    for (tempIndex = width; --tempIndex > 0; leftLinks[tempIndex] =
+        rightLinks[tempIndex] = tempIndex) {
+      mazeString.append("._"); // top walls
+    }
+    mazeString.append("\n "); // Open wall for entrance at top left
+  }
+
+  private void mazeRows(StringBuilder mazeString,
+      int[] leftLinks,
+      int[] rightLinks, Random rand) {
+    int currentCell;
+    int tempIndex;
+    String first;
+    String second;
+
+    while (--height > 0) {
+      for (currentCell = width; --currentCell > 0;) {
+        if (currentCell != (tempIndex = leftLinks[currentCell - 1])
+            && rand.nextBoolean()) { // connect cell to cell on right?
+          rightLinks[tempIndex] = rightLinks[currentCell];
+          leftLinks[rightLinks[currentCell]] = tempIndex;
+          rightLinks[currentCell] = currentCell - 1;
+          leftLinks[currentCell - 1] = currentCell;
+          second = "."; // no wall to right
+        } else {
+          second = "|"; // wall to the right
+        }
+        if (currentCell != (tempIndex = leftLinks[currentCell])
+            && rand.nextBoolean()) { // omit down-connection?
+          rightLinks[tempIndex] = rightLinks[currentCell];
+          leftLinks[rightLinks[currentCell]] = tempIndex;
+          leftLinks[currentCell] = currentCell;
+          rightLinks[currentCell] = currentCell;
+          first = "_"; // wall downward
+        } else {
+          first = " "; // no wall downward
+        }
+        mazeString.append(first);
+        mazeString.append(second);
+      }
+      mazeString.append("\n|");
+    }
+  }
+
+  private void mazeBottomRowAndExit(StringBuilder mazeString, int[] leftLinks,
+      int[] rightLinks, Random rand) {
+    int currentCell;
+    int tempIndex;
+
+    for (currentCell = width; --currentCell > 0;) {
+      if (currentCell != (tempIndex = leftLinks[currentCell - 1])
+          && (currentCell == rightLinks[currentCell] || rand.nextBoolean())) {
+        leftLinks[rightLinks[tempIndex] = rightLinks[currentCell]] = tempIndex;
+        leftLinks[rightLinks[currentCell] = currentCell - 1] = currentCell;
+        mazeString.append("_."); // no wall to right
+      } else {
+        if (currentCell == 1) {
+          mazeString.append("_."); // maze exit
+        } else {
+          mazeString.append("_|"); // regular wall to right
+        }
+      }
+      tempIndex = leftLinks[currentCell];
+      rightLinks[tempIndex] = rightLinks[currentCell];
+      leftLinks[rightLinks[currentCell]] = tempIndex;
+      leftLinks[currentCell] = currentCell;
+      rightLinks[currentCell] = currentCell;
+    }
+  }
+}
diff --git a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/StringBytesGlossary.java b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/StringBytesGlossary.java
index ce16225..e8676e8 100644
--- a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/StringBytesGlossary.java
+++ b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/StringBytesGlossary.java
@@ -192,6 +192,10 @@ public class StringBytesGlossary {
   @MakeImmutable
   public static final byte[] bLIMIT = stringToBytes("LIMIT");
 
+  // LolWutExecutor
+  @MakeImmutable
+  public static final byte[] bVERSION = stringToBytes("VERSION");
+
   // ********** Constants for Double Infinity comparisons **********
   public static final String P_INF = "+inf";
   public static final String INF = "inf";