-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Added Rest Client Module to benchmark Restful Web Services #756
Conversation
@busbey Please give feedback when you get some time. I have incorporated all your old comments. |
Hi @shivam-maharshi - I have been looking at this PR on and off. For me it is hard to review for a couple reasons:
I started looking into the RestWorkload.java, it is very different from CoreWorkload.java and so is not easy to view differences between the two with a diff. This reinforces the need for more documentation around what the motivations for this PR are and the benefits it brings. Some specific things that are confusing to me:
Sorry for the negative feedback but some things I would really like to see are more documentation and conversation about the work you've done, at this point there is too much change to really tell everything that is going on. Along with that it would be great to discuss if the changes you have made in RestWorkload.java could actually be integrated through additions to CoreWorkload.java and the bulk of the changes in the |
Hi @kruthar, thanks a lot for providing a detailed explanation. My aim is to eventually get this PR accepted. I am open to making any changes that are required to make this possible. Any feedbacks that will help me move in this direction are more than welcome! Here are the answers to some of your concerns.
I have performed a detailed study into the available free opensource tools most suited for benchmarking cloud webservices, and found that there is a lack of such a tool (My MS thesis is on a related topic). Hence I finally decided to add this module to YCSB since it catered to my need and I feel that many developers would be thrilled to use it. They can also easily tweak it to fit thier own usecase.
I totally understand your concerns and I am more than happy to discuss with you the reasoning behind this binding and the design opted. We can have a conversation on phone/email/skype or any other convenient option as you prefer. If you or anyone else have better suggestions regarding design and we would need to restructure the code, I would be more than happy to do that myself as well. Thanks again for your response. Hoping to hear back soon again. |
@shivam-maharshi - thanks for the background info and context, it makes a lot more sense now why this type of workload needs to be so different from the traditional CoreWorkload. I think you have also worked in enough flexibility to work with many potential REST layouts by using the trace files, a little more work for the user up front, but it means they can be sure it will hit all the endpoints they want it to and they dont seem to have to heterogenous. @busbey - there are several things that need to be updated in this PR before being merged in, but do you have any problems with trying to get this one in for 0.10.0 as an Untested binding? I would put it in as untested to start with because of the fact that it introduces a new Workload type, and I don't have a REST services on hand to test this thing. |
The rest module will pick up URLs from the above file according to | ||
the `requestdistribution` property (default is zipfian) mentioned in | ||
the rest_workload. In the example above we assume that the property `url.prefix` | ||
(see below for property description) is set to ``. If url.prefix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - there are two backticks here that are throwing off formatting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed it by using empty keyword instead of ``.
Can you rebase this PR to pick up the new ycsb shell scripts, then add a line for this rest binding in bin/bindings.properties. The python tool at bin/ycsb will be deprecated out at some point, but you might as well update that also with a line for your binding. |
Would also be greatly appreciated if you could rebase your PR done to just a handful of meaningful commits. Please prefix each commit with |
@kruthar Rebased the PR and updated bin/binding.properties and python bin/ycsb to include rest module. Also squashed all the different commits into a single meaningful commit and used [rest] in the message log. Please let me know if I need to do anything else you need from me. |
@kruthar The sigar thing is just a warning. It doesn't actually fail anything. The real issue in the test is
The real issue is the JVM crashed or hung or something. Maybe out of memory or something from Travis?
|
I'd rather take the time to get it right before merging. 0.11.0 will come out in short order after 0.10.0. |
Sorry, I am not sure that sigar is only suitable for jdk8. And if we do not have sigar files, there will be exception information in the console: no libsigar-universal64-macosx.dylib in java.library.path. But as the above said, maybe Cassandra can still startup (I am not sure). |
@@ -83,6 +83,7 @@ DATABASES = { | |||
"orientdb" : "com.yahoo.ycsb.db.OrientDBClient", | |||
"rados" : "com.yahoo.ycsb.db.RadosClient", | |||
"redis" : "com.yahoo.ycsb.db.RedisClient", | |||
"rest" : "com.yahoo.ycsb.webservice.rest.RestClient", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - line this up
license edit in
Does anyone else have a REST service just laying around to test this with? |
@kruthar Incorporated feedbacks. For starters you can just test read operations using 5 Google URLs in the trace file like this: url.prefix=https://www.google.com/#q= TraceFile: To avoid getting your IP blocked you can make 1 request every 2 seconds with total operation count of 100. For full fledged testing you can install MediaWiki (PHP, Apache WebServer, MySQL) and create some sample pages. It supports Create and Delete operations too. |
@kruthar Please let me know if you need some help setting a dummy Rest service. We can work together on verifying this anytime you are free. |
as an alternative for unblocking testing, what if we add an integration test that stands up an embedded REST service and then runs against it? |
@busbey We are using Wiremock for imitating Rest services (here: https://github.com/brianfrankcooper/YCSB/pull/756/files#diff-237d7ac48706b45c30b7c2ce9cec005eR97) in the RestClientTests, which take care of the HTTP handling part. It can also be used for integration test cases. On a side note I can add unit tests for RestWorkload to increase test coverage, thereby making sure that this module works as integration test cases might take sometime. |
I saw the use of Wiremock for the unit tests, I was hoping for something more thorough for an integration test. Mocks are great for verifying well-encapsulated assumptions about the client, but tend to miss out on the sorts of edge cases that come from talking to an actual system. I was hoping for something like an embedded tomcat demo or the like. NBD if there isn't something on hand. We'll just need to wait for somebody to have time to test things out. (that needn't be @kruthar btw, or even any of the existing project moderators. we accept reviews from anybody.) |
042ab93
to
2be10a4
Compare
@busbey @kruthar I've added integration test cases using embedded Tomcat 8. These tests start the embedded server (once at class loading) which exposes a mock RESTFul service used for benchmarking. Benchmarking is invoked using Client class from the core module to generate load using the RestWorkload. Everything works as expected. Please review the changes and let me know. |
Moderators, I've confirmed the working of the rest-binding both manually as well as using integration test cases. Please let me know if it needs more changes to get accepted. |
@risdenk @jasontedor Can you please take a look at this pull request as the other moderators are busy. |
@shivam-maharshi I'll try to look on Friday. |
@risdenk thanks. Even though integration tests runs fine, please let me know if you need some help testing this on a real RESTFul service. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some minor things that need changing and then can do another look over.
|
||
git clone git://github.com/brianfrankcooper/YCSB.git | ||
cd YCSB | ||
mvn -pl com.yahoo.ycsb:solr-binding -am clean package |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This refers to solr-binding instead of rest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected
public final ExpectedSystemExit exit = ExpectedSystemExit.none(); | ||
|
||
private static int port = 8080; | ||
private static Tomcat tomcat = new Tomcat(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be in the init()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved.
|
||
private static int port = 8080; | ||
private static Tomcat tomcat = new Tomcat(); | ||
private static final String BASE_DIR = System.getProperty("user.dir"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to get the paths to these files with Java resources. Some psuedo code:
this.class.getClassLoader().getResource()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Code Snippet - IntegrationTest.class.getClassLoader().getResource("error_trace.txt").getPath();
@BeforeClass | ||
public static void init() throws ServletException, LifecycleException, FileNotFoundException, IOException, | ||
DBException, InterruptedException { | ||
String webappDirLocation = "WebContent/"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can WebContent be moved to test/resources?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved
* demonstrated. | ||
*/ | ||
// Test cases pass for JDK8 but fail for JDK7 due to WireMock. | ||
@Ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole test class is ignored?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wiremock (The framework used to mock rest services for RestClientTest) requires JDK8 support. Hence this class is ignored as the tests fail on JDK7 but pass on JDK8.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to ignore tests for a specific JDK. See https://github.com/brianfrankcooper/YCSB/blob/master/cassandra/pom.xml#L67
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That will ignore all tests (including IntegrationTests) to skip for JDK 1.7. So I skipped only RestClientTest for JDK < 1.8 only. Change: https://github.com/brianfrankcooper/YCSB/pull/756/files#diff-237d7ac48706b45c30b7c2ce9cec005eR109
@BeforeClass | ||
public static void init() throws FileNotFoundException, IOException, DBException { | ||
Properties props = new Properties(); | ||
props.load(new FileReader("src/test/resources/workload_rest")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again should be able to use resources loading instead of a full path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Code Snippet -
props.load(new FileReader(RestClientTest.class.getClassLoader().getResource("workload_rest").getPath()));
@@ -0,0 +1,300 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think more time needs to be spent removing this RestWorkload file. I think it overlaps with CoreWorkload.
The format of CoreWorkload and RestWorkload of how it documents the properties and defines variables is different I think. Maybe try to make them more similar?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you that it would be nice to have RestWorkload merged into CoreWorkload but there are some concerns/points:
- In order to benchmark REST-full web services we must already have URL endpoints to benchmark. This is why we have URL maps in RestWorkload. However the way core workload is written currently, it generates keys to benchmark instead of reading them from a collection of predefined keys. Which definitely makes sense for DBs but not for web services. This way DB benchmarking is inherently pretty different from web services benchmarking.
- Considering the point above, if we still merge RestWorkload into CoreWorkload, it would require us to distinguish the key generation logic on the basis of some config value which would make the CoreWorkload code ugly (if-else blocks) and couple it tightly with logic specific to a particular binding.
- Delete Operations are not supported in CoreWorload, they need special handling for WebServices to benchmark.
- Since CoreWorkload is the central component, updating it will require an exhaustive testing for all the other bindings. This is precisely what I wanted to avoid.
- To minimize code duplication RestWorkload extends CoreWorkload. Hence a lot of code is being reused and most future updates in CoreWorkload will be propagated to RestWorkload automatically thereby reducing extra code efforts.
I understand the urge to have a single workload but given the different nature of WebServices there are some inherent differences from databases which makes this task a bit non-trivial. I am open to further improve this design in the future but for now, if we can get this pull request incorporated in the master then I will create another pull request with the improved design later. I would be more than happy to discuss an improved design with you, @kruthar and @busbey.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough we can always improve it later.
private static final String UPDATE_ZIPFIAN_CONSTANT_DEAFULT = "0.99"; | ||
private static final String UPDATE_RECORD_COUNT_PROPERTY = "updaterecordcount"; | ||
|
||
private static Map<Integer, String> readUrlMap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these all be static? Can these be accessed by different threads?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! They will be accessed by different threads but only for reading. But yes it makes sense to not keep them static as a single YCSB instance can be used for multiple benchmarking with different traces. Change incorporated.
<dependency> | ||
<groupId>org.apache.httpcomponents</groupId> | ||
<artifactId>httpclient</artifactId> | ||
<version>4.5.1</version> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency, can you pull all the versions into the properties above like you did for tomcat and jersey?
@shivam-maharshi - Can you ping this thread after you update? Thanks! |
075f789
to
02480e6
Compare
@risdenk I have incorporated all your feedbacks except for one (Merging RestWorkload into CoreWorkload.). Please take a look. Thanks for reviewing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shivam-maharshi Sorry for the delay in getting back to this. Finally had some time to review this in some detail. Added some comments. Thanks for your patience.
<junit.version>4.12</junit.version> | ||
<system-rules.version>1.16.0</system-rules.version> | ||
<wiremock.version>1.57</wiremock.version> | ||
<skipTests>true</skipTests> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason skipTests is here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. An error during commit. Build was passing.
</dependency> | ||
</dependencies> | ||
|
||
<build> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this plugin need to be here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. Module builds without this plugin too.
@@ -0,0 +1,300 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough we can always improve it later.
keychooser = new ZipfianGenerator(recordCount, zipfContant); | ||
} else if (requestDistrib.compareTo("latest") == 0) { | ||
// TODO: To be implemented. | ||
System.err.println("Latest request distribution is not supported"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should exit instead of returning null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error is printed without throwing a WorkloadException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handled - throw new WorkloadException("Latest request distribution is not supported for RestWorkload.");
private static NumberGenerator getKeyChooser(String requestDistrib, int recordCount, double zipfContant, | ||
Properties p) throws WorkloadException { | ||
NumberGenerator keychooser = null; | ||
if (requestDistrib.compareTo("exponential") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
each of these .compareTo
should just be .equals()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May even be cleaner as a switch case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same opinion but kept it consistent with the older CoreWorkload code. Updated to switch statements now.
*/ | ||
private static synchronized Map<Integer, String> getTrace(String filePath, int recordCount) | ||
throws WorkloadException { | ||
Map<Integer, String> urlMap = new ConcurrentHashMap<Integer, String>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a synchronized method does this need to be ConcurrentHashMap?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed both synchronized and concurrent from here since it is a read-only map and this workload method is invoked just once during client creation. Thanks for pointing out.
int responseCode; | ||
try { | ||
responseCode = httpExecute(new HttpPut(urlPrefix + endpoint), values.get("data").toString()); | ||
} catch (Exception e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is catching all exceptions and not actually printing what happened? Would be nice to log the actual exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be able to just add the logging to the handleExceptions method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
this.timedout.setIsSatisfied(true); | ||
} catch (InterruptedException e) { | ||
// Do nothing. Kept just to remove Check-style build error. | ||
byte a = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm wonder if there is a better way to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By updating checkstyle.xml with the below snippet a block now can have some text as opposed to just code to be considered valid:
<module name="EmptyBlock"> <property name="option" value="text"/> </module>
private static final String WORKLOAD_FILEPATH = IntegrationTest.class.getClassLoader().getResource("workload_rest").getPath(); | ||
private static final String TRACE_FILEPATH = IntegrationTest.class.getClassLoader().getResource("trace.txt").getPath(); | ||
private static final String ERROR_TRACE_FILEPATH = IntegrationTest.class.getClassLoader().getResource("error_trace.txt").getPath(); | ||
private static final String RESULTS_FILEPATH = IntegrationTest.class.getClassLoader().getResource("").getPath() + "results.txt"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the getResource("")
calls shouldn't be to ""
they should be to the proper file or directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file results doesn't exist hence we can't call getResource("results.txt");
. Since we need the folder path to /src/test/resources even getResource("resources")
doesn't work. Hence I've used a period - '.' signifying the current target directory.
6341401
to
8339804
Compare
@risdenk I have incorporated all your feedbacks. The build is failing due to some Cassandra test cases. Can you please take a look. Additionally, someone reached out to me asking for REST Web Services support in YCSB (https://github.com/shivam-maharshi/YCSB4WebServices/issues/1) which means it should turn out to be useful :) Let me know your feedbacks. |
@shivam-maharshi - kicked off the test again and looks like it failed related to the rest piece:
|
@risdenk This error occurs sometime in JDK8 and sometime in JDK7 intermittently. It because some request in the RestClientTest is taking more than 10 seconds to execute and hence timer thread kills it and throws an Error Status. Right now the timeout is 10 seconds which I thought would be enough. What do you propose we do to resolve this? I am going to increase the timeout to 20 seconds and see if that passes. However in long run this is bad to build runtime. If you have any suggestions to handle this elegantly, please let me know. |
8339804
to
75405a9
Compare
@risdenk All checks pass now. Please take a look and lemme know. |
@shivam-maharshi Travis CI for whatever reason tends to test really slowly. This seems to cause a lot of issues with Accumulo as well. Raising the timeout shouldn't hurt right? If it the test finishes then the timeout has no effect. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shivam-maharshi This is getting really close. One minor item related to where messages are printed. Can you also squash all your commits and make sure the one commit starts with [rest].
|
||
private int handleExceptions(Exception e, String url, String method) { | ||
if (logEnabled) { | ||
System.out.println(new StringBuilder(method).append(" Request: ").append(url).append(" | ") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The System.out
in a few places should be System.err
to make sure that messages go to stderr. I think there are a few examples of this in other bindings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@risdenk Let me quickly fix the logging and squash all the commit with [rest] today itself. Will notify you with the next commit. Thanks!
598b7ee
to
796a031
Compare
@risdenk Incorporated feedbacks and squashed all the commits into one. Please take a look and lemme know. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still a few System.out
instead of System.err
.
responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.POST); | ||
} | ||
if (logEnabled) { | ||
System.out.println(new StringBuilder("POST Request: ").append(urlPrefix).append(endpoint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.out
instead of System.err
responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.DELETE); | ||
} | ||
if (logEnabled) { | ||
System.out.println(new StringBuilder("DELETE Request: ").append(urlPrefix).append(endpoint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.out
instead of System.err
responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.GET); | ||
} | ||
if (logEnabled) { | ||
System.out.println(new StringBuilder("GET Request: ").append(urlPrefix).append(endpoint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.out
instead of System.err
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@risdenk these are not error messages but just information logs. That's why I didn't update them since they will carry the messages of all the HTTP codes including success like 200, 204 etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure any logging that isn't the measurements has usually gone to stderr. Maybe @busbey can shed some light here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@risdenk if that's the norm we follow in YCSB then lemme make this change real quick and notify you back.
responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.PUT); | ||
} | ||
if (logEnabled) { | ||
System.out.println(new StringBuilder("PUT Request: ").append(urlPrefix).append(endpoint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.out
instead of System.err
796a031
to
ef8c9f1
Compare
@risdenk I have updated all System.out to System.err. On of the three builds failed due to "build termination". Please take a look and lemme know. Thanks! |
@shivam-maharshi looks good to me. Thanks! |
Please review. Touched only the absolutely necessary files required. CheckStyle formatting is followed, unit tests created, build passes.