Skip to content

Conversation

@darionyaphet
Copy link

@darionyaphet darionyaphet commented Oct 8, 2016

What is this PR for?

Support Redis interpreter

What type of PR is it?

Feature

Todos

  • - Support Redis Cluster

What is the Jira issue?

How should this be tested?

Outline the steps to test the PR here.

Screenshots (if appropriate)

Questions:

  • Does the licenses files need update?
  • Is there breaking changes for older versions?
  • Does this needs documentation?

Copy link
Contributor

@anthonycorbacho anthonycorbacho left a comment

Choose a reason for hiding this comment

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

Thank you for bringing Redis Interpreter! this is awesome!

I did a quick review on your code please check it and let me know what you think.


private Jedis client;

private enum RedisCommand {
Copy link
Contributor

@anthonycorbacho anthonycorbacho Oct 8, 2016

Choose a reason for hiding this comment

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

As a redis user, i am expecting to use other redis command like hkeys, range and stuff.
Maybe its a good idea to add more here or come up with a better approach ?

Copy link
Author

Choose a reason for hiding this comment

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

Actually I'm not sure is there any good way to running redis command so I just simply create GET and SET :)

Copy link
Author

Choose a reason for hiding this comment

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

maybe I can use redis.clients.jedis.Protocol.Command to replacement RedisCommand

Copy link
Contributor

@anthonycorbacho anthonycorbacho Oct 9, 2016

Choose a reason for hiding this comment

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

yes, i think its better to use this enum
i was thinking about doing something like:

private final HashSet<String> redisCommands;

public static HashSet<String> getRedisCommands() {
  HashSet<String> hashSetOfRedisCommands = new HashSet<String>();

  for (Command c : Command.values()) {
      hashSetOfRedisCommands.add(c.name());
  }

  return hashSetOfRedisCommands;
}

public RedisInterpreter(Properties property) {
  super(property);
  redisCommands = getRedisCommands();
}

// then use this like 
redisCommands.contains(tokens[1]);

what do you think?

Copy link
Author

@darionyaphet darionyaphet Oct 9, 2016

Choose a reason for hiding this comment

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

Maybe we don't need a redis commands set to check whether the input command is illegal because there is a default case in switch . Is it a better one?

String[] tokens = splitTokens(input);
String keyword = tokens[0];

switch (Protocol.Command.valueOf(keyword)) {
            case GET:
                result = getProcess();
                break;
            case SET:
                result = setProcess();
                break;
            default:
                result = new InterpreterResult(ERROR, "key word not found");
                break;
 }

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it look better :)


@Override
public void open() {
client = new Jedis();
Copy link
Contributor

@anthonycorbacho anthonycorbacho Oct 8, 2016

Choose a reason for hiding this comment

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

I think here we should provide a better Redis client creation, as far as i know, this will assume that you are running redis locally, i think we can exploit more the interpreter setting creation, here an example with JDBC.
So user will be able to set host, port and credential.

Also how about using JedisPool instead of Jedis?

It will look like something like this

private static final int MAX_CONNECTION = 142;
private static final int MAX_IDLE = 10;
private static final int MIN_IDLE = 142;
private static final long MAX_WAIT = TimeUnit.SECONDS.toMillis(30);

private JedisPool jedisPool;

public void open() {
  jedisPool = new JedisPool(getPoolConfiguration(), property.getCacheHost(), property.getCachePort());
}

private JedisPoolConfig getPoolConfiguration() {
  JedisPoolConfig config = new JedisPoolConfig();
  config.setMaxTotal(MAX_CONNECTION);
  config.setMaxIdle(MAX_IDLE);
  config.setMinIdle(MIN_IDLE);
  config.setMaxWaitMillis(MAX_WAIT);

  return config;
}

// Utils method :)
private Jedis borrow() {
  return jedisPool.getResource();
}

// Then you can use it like
private InterpreterResult getProcess(String key) {
  Jedis jedis = borrow();
  Pipeline pipe = jedis.pipelined();
  Response<String> response = pipe.get(key);
  pipe.sync();
  jedis.close();

  String result = response.get();
  return new InterpreterResult(SUCCESS, result);
}

Copy link
Author

Choose a reason for hiding this comment

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

I read Jedis source code and find a strange case . The returnResource will be deprecated after 3.0 and the resource shoule be close after usage (don't return into source pool ) . Now I'm using 2.9 . I'm not sure it's a good idea .

Copy link
Author

Choose a reason for hiding this comment

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

Maybe using zeppelin to access Redis will not very frequently so create jedis client after it close is OK .

Copy link
Contributor

Choose a reason for hiding this comment

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

I think its better to use a Pool for this job, you only need jedis instance when you actually run the jobs, that is whay i am asking for changing from Redis to JedisPool, and in the doc i didnt find the notice about removing returnResource.

@@ -0,0 +1,98 @@
package org.apache.zeppelin.redis;
Copy link
Contributor

Choose a reason for hiding this comment

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

ASF Header is missing here.

import static org.apache.zeppelin.interpreter.InterpreterResult.Code.ERROR;
import static org.apache.zeppelin.interpreter.InterpreterResult.Code.SUCCESS;

public class RedisInterpreter extends Interpreter {
Copy link
Contributor

@anthonycorbacho anthonycorbacho Oct 8, 2016

Choose a reason for hiding this comment

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

Could you also add the class header with a description of this interpreter?
This will help futur contributors.


public class RedisInterpreter extends Interpreter {

private Logger LOG = LoggerFactory.getLogger(RedisInterpreter.class);
Copy link
Contributor

@anthonycorbacho anthonycorbacho Oct 8, 2016

Choose a reason for hiding this comment

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

Can we change this field for private static final Logger LOG = LoggerFactory.getLogger(RedisInterpreter.class);?

this will help to have one logger instance :)

@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 2 times, most recently from f40e716 to ecfa1b2 Compare October 9, 2016 09:45
@felixcheung
Copy link
Member

Hi - thank you for your contribution. Could you see the list of steps here:
https://zeppelin.apache.org/docs/0.6.1/development/writingzeppelininterpreter.html#contributing-a-new-interpreter-to-zeppelin-releases

Notably, could you add:

  • tests
  • documentation
  • list licenses for dependencies

@anthonycorbacho
Copy link
Contributor

@darionyaphet do you need some help for the change?

@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 2 times, most recently from 39e42ef to 244fe85 Compare October 12, 2016 07:16
@darionyaphet
Copy link
Author

Hi @anthonycorbacho thank you for you great help and I'm still working on it .

I have implement a lot of command include GEO ,HASH, HYPERLOGLOG, LIST, KEYS, Sets ,SORTED SET and Strings and have updated .

String keyword = tokens[0];

InterpreterResult result;
switch (Protocol.Command.valueOf(keyword)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Humm, that is a very massive switch case, i think we can come up with a more elegant way to to this kind of things,

since it mainly take the same argument String[] token, i guess we can go with the design pattern Strategy Pattern.

This idea here is to extract the logic to smaller classes and register then to a map.

eg:

// In the constructor
Map<Protocol.Command, Command> redisCommand = Maps.newHashMap<>();
// register command and class
redisCommand.put(GEOADD, GeoAdd.createInstance());
... // add other command

// create a java package org.apache.zeppelin.redis.cmd and add every implementation of redis cmd.
// but first you need to create a interface
// eg:
public interface Cmd {
  public boolean execute(String[] tokens);
}

// use this interface to implement the actual cmd
public class MyRedisCmd implements Cmd {
  public static MyRedisCmd createInstance() {
    return new MyredisCmd();
  }

  @Override
  public boolean execute(String[] tokens) {
   // do the work here
  }
}

// Once you have implemented the redis cmd, you can use this like this:
redisCommand.get(Protocol.Command.valueOf(keyword)).execute(tokens);

let me know what you think

Copy link
Author

Choose a reason for hiding this comment

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

@anthonycorbacho @kavinkumarks thanks for you reply with great suggest 👍 . Using Strategy Pattern to execute Redis Command will take me make a lot of classes , each Redis Command should have a class to express it . I'm not sure it's a good idea maybe the long switch list is more clearly to execute the simple redis commands in a single Java class .

what do you think :)

PS : I will append some test case about Redis Command later .

Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK, what @anthonycorbacho suggested seems more like a command pattern. (since it is about the action - what to do, not the algorithm - how to do)
Anyway +1 to apply what @anthonycorbacho suggested. Having more classes is not worse than having too long single class, (and also too long switch statement) especially they can be organized by package. We can even use static inner class to reduce the file count if we really want.

Btw, to be clear, I was the other one who want to make the Redis interpreter but at the design phase I was stuck on converting Redis command statement to Jedis method call.
(I was struggling to find a way to not having static mapping like this, but that was not easy as I thought.)
I will come up with clearer way sooner or later if possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I would prefer to have classes (will make test more easy) than a massive switch case. the main concern i have about big switch case tis that it is really hard to maintain and make the parent class huge. by de coupling the logic into smaller classes will improve the maintainability and make testability more easy.

Copy link
Author

@darionyaphet darionyaphet Oct 16, 2016

Choose a reason for hiding this comment

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

@anthonycorbacho @HeartSaVioR It's seems a good idea to split execute Redis Command into classes and this command classes will reused in Redis Cluster Interpreter. I have create a abstract class RedisCommand as a basic class :

public abstract class RedisCommand {

    private JedisPool clientPool;

    public RedisCommand(JedisPool clientPool) {
        this.clientPool = clientPool;
    }

    private Jedis borrowClient() {
        return clientPool.getResource();
    }

    private void releaseClient(Jedis client) {
        client.close();
    }

    public InterpreterResult execute(String[] tokens) {
        Jedis client = borrowClient();
        try {
            return execute(client, tokens);
        } catch (JedisException exception) {
            return new InterpreterResult(ERROR, exception.getMessage());
        } finally {
            releaseClient(client);
        }
    }

    /**
     * @param tokens
     * @return
     * @throws Exception
     */
    protected abstract InterpreterResult execute(Jedis client, String[] tokens);

}

I think it's OK and each redis command could extend this one .

@kavinkumarks
Copy link
Contributor

I too checked the changes and they look good.The comment from @anthonycorbacho about usage of "Strategy pattern" for selecting the relevant redis command instance and executing them is the better way to implement it.

Also, could we add some test cases for this?

Thanks,
Kavin
MailTo: [email protected]

@HeartSaVioR
Copy link
Contributor

Every command methods have a chance to leak Jedis instance when command is failing and throwing a kind of JedisException.
You need to replace your borrow-call-return pattern with try-with-resource thanks to Zeppelin requires Java 1.7.

@anthonycorbacho
Copy link
Contributor

@HeartSaVioR thank you for pointing that out, you are right we better handle the potential JedisException exception to avoid leak.

@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 2 times, most recently from 5ad9542 to 87a22cb Compare October 18, 2016 09:44
@darionyaphet
Copy link
Author

Hi @anthonycorbacho @HeartSaVioR I have update RedisInterpreter with some redis commands (include APPEND ,BITCOUNT ,BITFIELD ,BITOP, BITPOS, DECR) .

Please help review this one . If anything is OK , I will clean the commented code and completion other redis commands . Thanks ~~ :)

@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 2 times, most recently from e1c2f6c to 3b93b3e Compare October 20, 2016 14:05
Copy link
Contributor

@anthonycorbacho anthonycorbacho left a comment

Choose a reason for hiding this comment

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

Great to see this interpreter improving day by day!!
@HeartSaVioR what do you think?

return (String[]) list.toArray();
}

// ////////////////////////// KEYS //////////////////////////
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to remove comment here if there are not intentions of using the code below.

Copy link
Author

Choose a reason for hiding this comment

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

Hi I still working on Key commands . I will clean the unnecessary code after finished this PR :)

try {
return new InterpreterResult(SUCCESS, execute(client, tokens));
} catch (JedisException exception) {
return new InterpreterResult(ERROR, exception.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about adding a LOG.error here? something like
LOG.error("Couldn't execute redis command {}", token[0], e);

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

}
}
}
] No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

Need a new line here :)

client.close();
}
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

Need a new line here :)

if (COMMANDS.containsKey(command)) {
result = COMMANDS.get(command).execute(tokens);
} else {
result = new InterpreterResult(ERROR, "keyword not found");
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can provide a better error to the user like
Keyword " + tokens + " is not supportted?

What do you think?

Copy link
Author

Choose a reason for hiding this comment

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

Yep It's a good idea 👍

Copy link
Contributor

@HeartSaVioR HeartSaVioR left a comment

Choose a reason for hiding this comment

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

I left review comments but overall direction looks good unless we find brilliant idea to reduce verbose.

Please note that I couldn't check actual call of Jedis method since they're too many. I would like to see all tests instead, but tests don't necessary need to have files for each command.

Some of Jedis methods can return null (for example, if there's no matching result) which should be handled properly. Some of implementations of RedisCommands might throw NPE in some cases since you manipulate the result without null check. I'm not sure interpreter result type NULL is for these case.
@anthonycorbacho Could you clarify above this?
May be better to have generic return type for RedisCommand, and let RedisInterpreter handles null and also type-aware result.

md org.apache.zeppelin:zeppelin-markdown:0.6.1 Markdown support
postgresql org.apache.zeppelin:zeppelin-postgresql:0.6.1 Postgresql interpreter
python org.apache.zeppelin:zeppelin-python:0.6.1 Python interpreter
redis org.apache.zeppelin:zeppelin-redis:0.6.0 Redis interpreter
Copy link
Contributor

Choose a reason for hiding this comment

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

version mismatch

redis/pom.xml Outdated
<version>0.7.0-SNAPSHOT</version>

<properties>
<redis.version>2.9.0</redis.version>
Copy link
Contributor

Choose a reason for hiding this comment

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

It can make unneeded confusing. jedis.version would be better.

LOG.info("Connection to %s:%d", host, port);

// TODO(darion) : Need more config ?
config = new JedisPoolConfig();
Copy link
Contributor

Choose a reason for hiding this comment

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

Since Redis interpreter doesn't need to be super fast at all, testOnBorrow() can relieve the connection issue with ignorable overhead.

import static org.apache.zeppelin.interpreter.InterpreterResult.Code.SUCCESS;

/**
* Redis BLPOPCommand
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong explanation here.

}

public InterpreterResult execute(String[] tokens) {
Jedis client = borrowClient();
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use try-with-resource:

try (Jedis client = clientPool.getResource()) {
    ...
}

No need to have borrowClient() and releaseClient(), and also no need to explicitly handling closing resource.

try {
return new InterpreterResult(SUCCESS, execute(client, tokens));
} catch (JedisException exception) {
return new InterpreterResult(ERROR, exception.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

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

+1


@Override
protected String execute(Jedis client, String[] tokens) {
String result = client.geoadd(tokens[1], Double.valueOf(tokens[2]),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer wrapping it with String.valueOf() or call toString() if it's same, but this is simpler so OK to me.

Copy link
Contributor

@HeartSaVioR HeartSaVioR Oct 22, 2016

Choose a reason for hiding this comment

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

And I also prefer returning it without having local variable (this style occurs all of RedisCommand implementations), but this way makes it easier to set a breakpoint. I'd like to see other's opinions here.

Copy link
Author

@darionyaphet darionyaphet Oct 27, 2016

Choose a reason for hiding this comment

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

toString maybe better and reasonable . @HeartSaVioR


@Override
protected String execute(Jedis client, String[] tokens) {
String[] keys = new String[tokens.length - 2];
Copy link
Contributor

Choose a reason for hiding this comment

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

Please extract this logic to method of utility class or method of RedisCommand. This is duplicated from many places. The logics which manipulate keys/parameters are all candidates to extract.

@anthonycorbacho
Copy link
Contributor

@HeartSaVioR yeah i would love to have a class that manipulate redis cmd and handle null, ussually i am not a huge fan of returning null, i would prefer to return an EMPTY type instead.

@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 2 times, most recently from 4ecd614 to 2937c92 Compare October 27, 2016 08:53
@darionyaphet
Copy link
Author

Aha. A lot of files conflict and I can't resolve that . Maybe I will close this one and reopen another .

@HeartSaVioR
Copy link
Contributor

HeartSaVioR commented Oct 27, 2016

Follow this if rebase with master is hard due to lots of conflicts:

  1. create a new branch which is based on current master
  2. cherry-pick your commits (write down your last commit id)
  3. change branch to PR's branch
  4. git reset --hard last commit id
  5. git push --force

last commit id might be replaced with branch name you created, but I didn't try it.

@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 4 times, most recently from 472c22d to d319009 Compare October 27, 2016 10:07
@darionyaphet darionyaphet force-pushed the ZEPPELIN-1233 branch 5 times, most recently from 3e8a5c2 to 03cd13a Compare October 27, 2016 10:49
@darionyaphet
Copy link
Author

Add a Screenshot
1539610b-0afe-4cd0-9a3a-abb208adfb7d

Support Redis interpreter

Feature

 Support Redis Cluster

- https://issues.apache.org/jira/browse/ZEPPELIN-1233

Outline the steps to test the PR here.

- Does the licenses files need update?
- Is there breaking changes for older versions?
- Does this needs documentation?
@asfgit asfgit closed this in c38a0a0 May 9, 2018
asfgit pushed a commit that referenced this pull request May 9, 2018
close #83
close #86
close #125
close #133
close #139
close #146
close #193
close #203
close #246
close #262
close #264
close #273
close #291
close #299
close #320
close #347
close #389
close #413
close #423
close #543
close #560
close #658
close #670
close #728
close #765
close #777
close #782
close #783
close #812
close #822
close #841
close #843
close #878
close #884
close #918
close #989
close #1076
close #1135
close #1187
close #1231
close #1304
close #1316
close #1361
close #1385
close #1390
close #1414
close #1422
close #1425
close #1447
close #1458
close #1466
close #1485
close #1492
close #1495
close #1497
close #1536
close #1545
close #1561
close #1577
close #1600
close #1603
close #1678
close #1695
close #1739
close #1748
close #1765
close #1767
close #1776
close #1783
close #1799
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.

5 participants