diff --git a/README.md b/README.md index 42a78c699..413fa9c42 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Support:** [![Join the chat at https://gitter.im/intuit/wasabi](https://badges.gitter.im/intuit/wasabi.svg)](https://gitter.im/intuit/wasabi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
**Documentation:** [User Guide](https://intuit.github.io/wasabi/v1/guide/index.html), [JavaDocs](https://intuit.github.io/wasabi/v1/javadocs/latest/)
-**A/B Testing Overview:** [![A/B Testing Overview](http://img.shields.io/badge/video-A%2FB%20Testing%20Overview-red.svg)](https://www.youtube.com/watch?v=_HtvJwBPUqk&feature=youtu.be)
+**A/B Testing Overview:** [![A/B Testing Overview](http://img.shields.io/badge/video-A%2FB%20Testing%20Overview-red.svg)](https://www.youtube.com/watch?v=_HtvJwBPUqk&feature=youtu.be) [![Blog Meet Wasabi](https://img.shields.io/badge/blog-Meet%20Wasabi-brightgreen.svg)](https://medium.com/blueprint-by-intuit/open-sourcing-wasabi-the-a-b-testing-platform-by-intuit-a8d5abc958d) [![Blog Architecture Behind Wasabi](https://img.shields.io/badge/blog-Architecture%20Behind%20Wasabi-orange.svg)](https://medium.com/blueprint-by-intuit/the-architecture-behind-wasabi-an-open-source-a-b-testing-platform-b52430d3fd80)
**Continuous Integration:** [![Build Status](https://api.travis-ci.org/intuit/wasabi.svg?branch=develop)](https://travis-ci.org/intuit/wasabi) [![Coverage Status](https://coveralls.io/repos/github/intuit/wasabi/badge.svg)](https://coveralls.io/github/intuit/wasabi?branch=develop) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.intuit.wasabi/wasabi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.intuit.wasabi/wasabi)
@@ -359,6 +359,29 @@ Code changes can readily be verified by running the growing collection of includ ```bash % ./bin/wasabi.sh start test stop ``` +##### Troubleshooting + +Integration tests might fail intermittently due to a time drift issue in docker containers on Mac OSX. + +When the Mac sleeps and wakes back up, there is a lag created between the clock in the Mac vs the +running docker containers. This is a known issue in Docker for Mac. + +This can be fixed by running the following command: + +```bash +% docker run --rm --privileged alpine hwclock -s +``` + +The above command will need to be run every time when there is a time drift. + +To automatically run this command and update the time each time the Mac wakes up, you could install +the following agent: + +```bash +% curl https://raw.githubusercontent.com/arunvelsriram/docker-time-sync-agent/master/install.sh | bash +``` + +You can read more about this at: [quick-tip-fixing-time-drift-issue-on-docker-for-mac](https://blog.shameerc.com/2017/03/quick-tip-fixing-time-drift-issue-on-docker-for-mac) ## Package and Deploy at Scale diff --git a/modules/analytics-objects/src/main/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetail.java b/modules/analytics-objects/src/main/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetail.java index 91dfd32ac..cc03cf8bf 100644 --- a/modules/analytics-objects/src/main/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetail.java +++ b/modules/analytics-objects/src/main/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetail.java @@ -24,6 +24,7 @@ import java.util.Date; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -56,6 +57,8 @@ public class ExperimentDetail { private String description; + private Set tags; + /** * This class holds the details for the Buckets. This is especially interesting for @@ -209,7 +212,8 @@ public void setDescription(String description) { */ public ExperimentDetail(Experiment exp) { this(exp.getID(), exp.getState(), exp.getLabel(), exp.getApplicationName(), - exp.getModificationTime(), exp.getStartTime(), exp.getEndTime(), exp.getDescription()); + exp.getModificationTime(), exp.getStartTime(), exp.getEndTime(), exp.getDescription(), + exp.getTags()); } /** @@ -223,10 +227,11 @@ public ExperimentDetail(Experiment exp) { * @param startTime the startTime of the experiment to determine the winner so far * @param endTime the endtime of the experiment * @param description the description of the experiment + * @param tags the tags of the experiment */ public ExperimentDetail(Experiment.ID id, Experiment.State state, Experiment.Label label, Application.Name appName, Date modificationTime, Date startTime, - Date endTime, String description) { + Date endTime, String description, Set tags) { setId(id); setState(state); setLabel(label); @@ -235,6 +240,7 @@ public ExperimentDetail(Experiment.ID id, Experiment.State state, Experiment.Lab setStartTime(startTime); setEndTime(endTime); setDescription(description); + setTags(tags); } public Experiment.ID getId() { @@ -337,6 +343,14 @@ public void setDescription(String description) { this.description = description; } + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + /** * This method takes a list of buckets and transforms it to the {@link BucketDetail}s that are needed * for later extension. diff --git a/modules/analytics-objects/src/test/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetailTest.java b/modules/analytics-objects/src/test/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetailTest.java index a36754fbe..fcd5a2fcc 100644 --- a/modules/analytics-objects/src/test/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetailTest.java +++ b/modules/analytics-objects/src/test/java/com/intuit/wasabi/analyticsobjects/wrapper/ExperimentDetailTest.java @@ -24,6 +24,8 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; @@ -43,6 +45,7 @@ public class ExperimentDetailTest { private static Calendar modTime = Calendar.getInstance(); private static Calendar startTime = Calendar.getInstance(); private static Calendar endTime = Calendar.getInstance(); + private static Set tags = new TreeSet<>(java.util.Arrays.asList("tag1", "tag2", "tag3")); private Experiment exp = Experiment.withID(expId).withApplicationName(appName).withModificationTime(modTime.getTime()) @@ -58,7 +61,7 @@ public static void setUp() { @Test public void testConstructor() { ExperimentDetail expDetail = new ExperimentDetail(expId, expState, expLabel, appName, modTime.getTime(), - startTime.getTime(), endTime.getTime(), description); + startTime.getTime(), endTime.getTime(), description, tags); assertEquals(expDetail.getApplicationName(), appName); assertEquals(expDetail.getId(), expId); assertEquals(expDetail.getLabel(), expLabel); @@ -66,36 +69,37 @@ public void testConstructor() { assertEquals(expDetail.getModificationTime(), modTime.getTime()); assertEquals(expDetail.getEndTime(), endTime.getTime()); assertEquals(expDetail.getDescription(), description); + assertEquals(expDetail.getTags(), tags); } @Test(expected = IllegalArgumentException.class) public void testConstraintsId() { new ExperimentDetail(null, expState, expLabel, appName, modTime.getTime(), - startTime.getTime(), endTime.getTime(), description); + startTime.getTime(), endTime.getTime(), description, tags); } @Test(expected = IllegalArgumentException.class) public void testConstraintsState() { new ExperimentDetail(expId, null, expLabel, appName, modTime.getTime(), - startTime.getTime(), endTime.getTime(), description); + startTime.getTime(), endTime.getTime(), description, tags); } @Test(expected = IllegalArgumentException.class) public void testConstraintsStateDeleted() { new ExperimentDetail(expId, Experiment.State.DELETED, expLabel, appName, modTime.getTime(), - startTime.getTime(), endTime.getTime(), description); + startTime.getTime(), endTime.getTime(), description, tags); } @Test(expected = IllegalArgumentException.class) public void testConstraintsLabel() { new ExperimentDetail(expId, expState, null, appName, modTime.getTime(), - startTime.getTime(), endTime.getTime(), description); + startTime.getTime(), endTime.getTime(), description, tags); } @Test(expected = IllegalArgumentException.class) public void testConstraintsAppName() { new ExperimentDetail(expId, expState, expLabel, null, modTime.getTime(), - startTime.getTime(), endTime.getTime(), description); + startTime.getTime(), endTime.getTime(), description, tags); } @Test diff --git a/modules/api/src/main/java/com/intuit/wasabi/api/ApplicationsResource.java b/modules/api/src/main/java/com/intuit/wasabi/api/ApplicationsResource.java index 5800966d6..d303491e5 100644 --- a/modules/api/src/main/java/com/intuit/wasabi/api/ApplicationsResource.java +++ b/modules/api/src/main/java/com/intuit/wasabi/api/ApplicationsResource.java @@ -19,7 +19,9 @@ import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.intuit.wasabi.authenticationobjects.UserInfo; import com.intuit.wasabi.authorization.Authorization; +import com.intuit.wasabi.authorizationobjects.UserPermissions; import com.intuit.wasabi.exceptions.AuthenticationException; import com.intuit.wasabi.experiment.Experiments; import com.intuit.wasabi.experiment.Pages; @@ -43,8 +45,10 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static com.intuit.wasabi.api.APISwaggerResource.DEFAULT_MODEXP; import static com.intuit.wasabi.api.APISwaggerResource.EXAMPLE_AUTHORIZATION_HEADER; @@ -373,4 +377,42 @@ public Response getPagesAndAssociatedExperimentsForApplication( throw exception; } } + + /** + * Returns a Map of allowed Applications to the corresponding + * tags that have been stored with them. + * + * @param authorizationHeader the authorization headers + * @return Response object containing the Applications with their tags + */ + @GET + @Path("/tags") + @Produces(APPLICATION_JSON) + @ApiOperation(value = "Returns a Map of Applications to their tags", + response = Map.class) + @Timed + public Response getAllExperimentTags( + @HeaderParam(AUTHORIZATION) + @ApiParam(value = EXAMPLE_AUTHORIZATION_HEADER, required = true) + final String authorizationHeader) { + try { + UserInfo.Username userName = authorization.getUser(authorizationHeader); + Set allowed = new HashSet<>(); + + // get the for the user visible Applications + List authorized = authorization.getUserPermissionsList(userName).getPermissionsList(); + for (UserPermissions perm : authorized) { + allowed.add(perm.getApplicationName()); + } + + // get their associated tags + Map> allTags = experiments.getTagsForApplications(allowed); + + return httpHeader.headers().entity(allTags).build(); + } catch (Exception exception) { + LOGGER.error("Retrieving the Experiment tags failed with error:", + exception); + throw exception; + } + } } diff --git a/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilter.java b/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilter.java index 5d811e9e0..bd0da5173 100644 --- a/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilter.java +++ b/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilter.java @@ -21,6 +21,7 @@ import com.intuit.wasabi.api.pagination.filters.PaginationFilterProperty; import org.apache.commons.lang3.StringUtils; +import java.util.Arrays; import java.util.function.BiPredicate; import java.util.function.Function; @@ -68,7 +69,10 @@ public enum Property implements PaginationFilterProperty { start_time(ExperimentDetail::getStartTime, FilterUtil::extractTimeZoneAndTestDate), end_time(ExperimentDetail::getEndTime, FilterUtil::extractTimeZoneAndTestDate), date_constraint_start(ExperimentDetail::getStartTime, ExperimentFilter::constraintTest), - date_constraint_end(ExperimentDetail::getEndTime, ExperimentFilter::constraintTest); + date_constraint_end(ExperimentDetail::getEndTime, ExperimentFilter::constraintTest), + tags(ExperimentDetail::getTags, (tagsSet, filter) -> tagsSet.stream().anyMatch(tag + -> Arrays.asList(filter.split(";")).contains(tag))), + tags_and(ExperimentDetail::getTags, (tagsSet, filter) -> tagsSet.containsAll(Arrays.asList(filter.split(";")))); private final Function propertyExtractor; private final BiPredicate filterPredicate; diff --git a/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilter.java b/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilter.java index 5c55a87ae..10bbba266 100644 --- a/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilter.java +++ b/modules/api/src/main/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilter.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.StringUtils; import java.time.LocalDate; +import java.util.Arrays; import java.util.Date; import java.util.function.BiPredicate; import java.util.function.Function; @@ -73,7 +74,10 @@ public enum Property implements PaginationFilterProperty { state_exact(Experiment::getState, ExperimentFilter::stateTest), date_constraint_start(Experiment::getStartTime, ExperimentFilter::constraintTest), date_constraint_end(Experiment::getEndTime, ExperimentFilter::constraintTest), - favorite(Experiment::isFavorite, (isFavorite, filter) -> Boolean.parseBoolean(filter) == isFavorite); + favorite(Experiment::isFavorite, (isFavorite, filter) -> Boolean.parseBoolean(filter) == isFavorite), + tags(Experiment::getTags, (tagsSet, filter) -> tagsSet.stream().anyMatch(tag + -> Arrays.asList(filter.split(";")).contains(tag))), + tags_and(Experiment::getTags, (tagsSet, filter) -> tagsSet.containsAll(Arrays.asList(filter.split(";")))); private final Function propertyExtractor; private final BiPredicate filterPredicate; diff --git a/modules/api/src/test/java/com/intuit/wasabi/api/ApplicationsResourceTest.java b/modules/api/src/test/java/com/intuit/wasabi/api/ApplicationsResourceTest.java index 795bc310a..2293fd1c0 100644 --- a/modules/api/src/test/java/com/intuit/wasabi/api/ApplicationsResourceTest.java +++ b/modules/api/src/test/java/com/intuit/wasabi/api/ApplicationsResourceTest.java @@ -17,6 +17,9 @@ import com.intuit.wasabi.authenticationobjects.UserInfo; import com.intuit.wasabi.authorization.Authorization; +import com.intuit.wasabi.authorizationobjects.Permission; +import com.intuit.wasabi.authorizationobjects.UserPermissions; +import com.intuit.wasabi.authorizationobjects.UserPermissionsList; import com.intuit.wasabi.exceptions.AuthenticationException; import com.intuit.wasabi.experiment.Experiments; import com.intuit.wasabi.experiment.Pages; @@ -38,8 +41,13 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import static com.intuit.wasabi.authorizationobjects.Permission.READ; import static com.intuit.wasabi.authorizationobjects.Permission.UPDATE; @@ -51,6 +59,7 @@ import static org.mockito.Matchers.anyCollection; import static org.mockito.Mockito.anyObject; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -246,4 +255,43 @@ public void getExperimentsByPages() throws Exception { assertThat(pageExperimentsCaptor.getValue().size(), is(1)); assertThat(pageExperimentsCaptor.getValue(), hasEntry("experiments", pageExperiments)); } + + @Test + public void testTagsRetrieval() { + when(authorization.getUser("foo")).thenReturn(username); + + Iterator userPermIterator = mock(Iterator.class); + when(userPermIterator.hasNext()).thenReturn(true, true, true, false); + List permissions = Arrays.asList(Permission.READ); + when(userPermIterator.next()) + .thenReturn(UserPermissions.newInstance(Application.Name.valueOf("app01"), permissions).build()) + .thenReturn(UserPermissions.newInstance(Application.Name.valueOf("app02"), permissions).build()) + .thenReturn(UserPermissions.newInstance(Application.Name.valueOf("app03"), permissions).build()); + + + UserPermissionsList wrapClass = mock(UserPermissionsList.class); + List userPermList = mock(List.class); + when(wrapClass.getPermissionsList()).thenReturn(userPermList); + + when(userPermList.iterator()).thenReturn(userPermIterator); + when(authorization.getUserPermissionsList(username)).thenReturn(wrapClass); + + + when(httpHeader.headers()).thenReturn(responseBuilder); + when(responseBuilder.entity(anyCollection())).thenReturn(responseBuilder); + when(responseBuilder.build()).thenReturn(response); + + Map> tags = mock(HashMap.class); + + when(experiments.getTagsForApplications(applicationNames)).thenReturn(tags); + + // all Applications are allowed in this test + Set allowed = new HashSet<>(Arrays.asList(Application.Name.valueOf("app01"), + Application.Name.valueOf("app02"), Application.Name.valueOf("app03"))); + + applicationsResource.getAllExperimentTags("foo"); + + verify(experiments).getTagsForApplications(allowed); + + } } diff --git a/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilterTest.java b/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilterTest.java index dd8996596..3855ac9d7 100644 --- a/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilterTest.java +++ b/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentDetailFilterTest.java @@ -31,7 +31,9 @@ import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.TimeZone; +import java.util.TreeSet; /** @@ -54,9 +56,10 @@ public void setup() { Date startTime = cal.getTime(); cal.add(Calendar.DATE, 14); Date endTime = cal.getTime(); + Set tags = new TreeSet<>(java.util.Arrays.asList("tag1", "tag2", "tag3")); experimentDetail = new ExperimentDetail(expId, Experiment.State.RUNNING, expLabel, appName, - modTime, startTime, endTime, "testDescription"); + modTime, startTime, endTime, "testDescription", tags); Bucket b1 = Bucket.newInstance(expId, Bucket.Label.valueOf("Bucket1")).withAllocationPercent(0.6).build(); Bucket b2 = Bucket.newInstance(expId, Bucket.Label.valueOf("Bucket2")).withAllocationPercent(0.4).build(); @@ -71,8 +74,12 @@ public void setup() { @Parameterized.Parameters(name = "expDetailFilter({index})") public static Collection data() { return Arrays.asList(new Object[][]{ - {"experiment_label=ExperimentLabel", true}, {"bucket_label=Bucket2", true}, - {"application_name=testApp", false}, {"mod_time=Summer", false}, {"Experiment", true} + {"experiment_label=ExperimentLabel", true}, + {"bucket_label=Bucket2", true}, + {"application_name=testApp", false}, + {"mod_time=Summer", false}, + {"Experiment", true}, + {"tags=tag1", true}, }); } diff --git a/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilterTest.java b/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilterTest.java index 33d5a9aa5..87f57f607 100644 --- a/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilterTest.java +++ b/modules/api/src/test/java/com/intuit/wasabi/api/pagination/filters/impl/ExperimentFilterTest.java @@ -25,8 +25,10 @@ import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; @@ -56,6 +58,7 @@ public void setup() throws Exception { .withEndTime(endTime) .withState(Experiment.State.RUNNING) .withCreatorID("TheCreator") + .withTags(new HashSet<>(Arrays.asList("tag1", "tag2", "foo"))) .build(); } @@ -79,6 +82,9 @@ public void testTest() throws Exception { testCases.put("state_exact=notterminated", true); testCases.put("state_exact=non-terminated", false); testCases.put("state_exact=draft", false); + testCases.put("tags=tag2", true); + testCases.put("tags=tag42", false); + testCases.put("tags=foo;tag", true); for (Map.Entry testCase : testCases.entrySet()) { experimentFilter.replaceFilter(testCase.getKey(), "+0000"); diff --git a/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/CassandraDriver.java b/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/CassandraDriver.java index c51c390d2..537d46188 100644 --- a/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/CassandraDriver.java +++ b/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/CassandraDriver.java @@ -156,6 +156,18 @@ interface Configuration { */ int getPoolTimeoutMillis(); + /** + * Returns the defined connection timeout in milliseconds. + * @return the defined connection timeout in milliseconds + */ + int getConnectTimeoutMillis(); + + /** + * Returned the per-host read timeout in milliseconds from the configuration. + * @return the defined read timeout in milliseconds from the configuration + */ + int getReadTimeoutMillis(); + /** * Returns max connections per remote host * @return max connections per remote host diff --git a/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/DefaultCassandraDriver.java b/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/DefaultCassandraDriver.java index 8e114d6ee..fae796f42 100644 --- a/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/DefaultCassandraDriver.java +++ b/modules/cassandra-datastax/src/main/java/com/intuit/wasabi/cassandra/datastax/DefaultCassandraDriver.java @@ -27,6 +27,7 @@ import com.datastax.driver.core.QueryLogger; import com.datastax.driver.core.QueryOptions; import com.datastax.driver.core.Session; +import com.datastax.driver.core.SocketOptions; import com.datastax.driver.core.exceptions.DriverException; import com.datastax.driver.core.exceptions.InvalidQueryException; import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy; @@ -146,6 +147,17 @@ private void initialize() throws IOException { //Configure to use compression builder.withCompression(ProtocolOptions.Compression.LZ4); + + //The connection timeout in milliseconds. + //As the name implies, the connection timeout defines how long the driver waits to establish a new connection to a Cassandra node before giving up. + //Default value is 5000ms + builder.getConfiguration().getSocketOptions().setConnectTimeoutMillis(getConfiguration().getConnectTimeoutMillis()); + + //The per-host read timeout in milliseconds. + //This defines how long the driver will wait for a given Cassandra node to answer a query. + //Default value is 12000ms + builder.getConfiguration().getSocketOptions().setReadTimeoutMillis(getConfiguration().getReadTimeoutMillis()); + // SSL connection if (getConfiguration().useSSL()) { try { diff --git a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/Experiment.java b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/Experiment.java index a40548fbb..a7d3d1822 100644 --- a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/Experiment.java +++ b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/Experiment.java @@ -44,6 +44,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.UUID; import static com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT; @@ -99,6 +101,8 @@ public class Experiment implements Cloneable, ExperimentBase, Serializable { private Integer userCap; @ApiModelProperty(value = "creator of the experiment", required = false) private String creatorID; + @ApiModelProperty(value = "a set of experiment tags") + private Set tags; private Boolean favorite; @@ -318,6 +322,7 @@ public int hashCode() { .append(isRapidExperiment) .append(userCap) .append(creatorID) + .append(tags) .toHashCode(); } @@ -351,6 +356,7 @@ public boolean equals(Object obj) { .append(isRapidExperiment, other.getIsRapidExperiment()) .append(userCap, other.getUserCap()) .append(creatorID, other.getCreatorID()) + .append(tags, other.getTags()) .isEquals(); } @@ -429,6 +435,18 @@ public DateMidnight calculateLastDay() { return earliestDay; } + @Override + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + if (null != tags) + this.tags = new TreeSet<>(tags); + else + this.tags = tags; + } + //TODO: redesign state and state transition to be state machine public enum State { @@ -529,6 +547,7 @@ private Builder(Experiment other) { instance.isRapidExperiment = other.isRapidExperiment; instance.userCap = other.userCap; instance.creatorID = other.creatorID; + instance.tags = other.tags; } private Date copyDate(Date date) { @@ -564,31 +583,26 @@ public Builder withModelVersion(String modelVersion) { public Builder withCreationTime(final Date creationTime) { this.instance.creationTime = creationTime; - return this; } public Builder withModificationTime(final Date modificationTime) { instance.modificationTime = modificationTime; - return this; } public Builder withDescription(final String description) { instance.description = description; - return this; } public Builder withHypothesisIsCorrect(final String hypothesisIsCorrect) { instance.hypothesisIsCorrect = hypothesisIsCorrect; - return this; } public Builder withResults(final String results) { instance.results = results; - return this; } @@ -599,37 +613,31 @@ public Builder withRule(final String rule) { public Builder withSamplingPercent(final Double samplingPercent) { instance.samplingPercent = samplingPercent; - return this; } public Builder withStartTime(final Date startTime) { instance.startTime = startTime; - return this; } public Builder withEndTime(final Date endTime) { instance.endTime = endTime; - return this; } public Builder withState(final State state) { instance.state = state; - return this; } public Builder withLabel(final Experiment.Label label) { instance.label = label; - return this; } public Builder withApplicationName(final Application.Name appName) { instance.applicationName = appName; - return this; } @@ -638,6 +646,11 @@ public Builder withCreatorID(final String creatorID) { return this; } + public Builder withTags(final Set tags) { + instance.setTags(tags); + return this; + } + public Experiment build() { Experiment result = instance; instance = null; diff --git a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/ExperimentBase.java b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/ExperimentBase.java index 91926e33b..d33bb9842 100644 --- a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/ExperimentBase.java +++ b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/ExperimentBase.java @@ -16,6 +16,7 @@ package com.intuit.wasabi.experimentobjects; import java.util.Date; +import java.util.Set; /** * This interface is a quick workaround for the problem that {@link Experiment}, {@link PrioritizedExperiment} @@ -88,4 +89,11 @@ public interface ExperimentBase { */ Boolean getIsPersonalizationEnabled(); + /** + * Returns the set of tags associated with ths experiment. + * + * @return a sorted set of Strings for all labels stored with the experiment. + */ + Set getTags(); + } diff --git a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/NewExperiment.java b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/NewExperiment.java index 01446cf11..7524a297b 100644 --- a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/NewExperiment.java +++ b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/NewExperiment.java @@ -23,6 +23,8 @@ import org.apache.commons.lang3.builder.ToStringStyle; import java.util.Date; +import java.util.Set; +import java.util.TreeSet; /** * Specification of a new experiment @@ -64,6 +66,8 @@ public class NewExperiment implements ExperimentBase { private Integer userCap = Integer.MAX_VALUE; @ApiModelProperty(required = false) private String creatorID = ""; + @ApiModelProperty(value = "a set of experiment tags") + private Set tags; public NewExperiment(Experiment.ID id) { super(); @@ -223,7 +227,6 @@ public Application.Name getApplicationName() { } public void setApplicationName(Application.Name value) { -// Preconditions.checkNotNull(value); applicationName = value; } @@ -232,10 +235,21 @@ public String getCreatorID() { } public void setCreatorID(String value) { -// Preconditions.checkNotNull(value); creatorID = value; } + @Override + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + if (null != tags) + this.tags = new TreeSet<>(tags); + else + this.tags = tags; + } + @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); @@ -317,6 +331,11 @@ public Builder withCreatorID(final String value) { return this; } + public Builder withTags(final Set tags) { + instance.setTags(tags); + return this; + } + public NewExperiment build() { new ExperimentValidator().validateNewExperiment(instance); diff --git a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/PrioritizedExperiment.java b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/PrioritizedExperiment.java index 8aa2cf9e5..6907c55a8 100644 --- a/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/PrioritizedExperiment.java +++ b/modules/experiment-objects/src/main/java/com/intuit/wasabi/experimentobjects/PrioritizedExperiment.java @@ -25,6 +25,8 @@ import java.io.Serializable; import java.util.Date; +import java.util.Set; +import java.util.TreeSet; /** * Object for holding an experiment with Priotization. @@ -69,9 +71,10 @@ public class PrioritizedExperiment implements Cloneable, ExperimentBase, Seriali private Integer userCap; @ApiModelProperty(required = false) private String creatorID = ""; - @ApiModelProperty(value = "priority within the application", required = true) private Integer priority; + @ApiModelProperty(value = "a set of experiment tags") + private Set tags; protected PrioritizedExperiment() { super(); @@ -250,6 +253,7 @@ public int hashCode() { .append(isRapidExperiment) .append(userCap) .append(creatorID) + .append(tags) .toHashCode(); } @@ -282,6 +286,7 @@ public boolean equals(Object obj) { .append(userCap, other.getUserCap()) .append(isRapidExperiment, other.getIsRapidExperiment()) .append(creatorID, other.getCreatorID()) + .append(tags, other.getTags()) .isEquals(); } @@ -306,6 +311,18 @@ public boolean isDeleted() { return state.equals(Experiment.State.DELETED); } + @Override + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + if (null != tags) + this.tags = new TreeSet<>(tags); + else + this.tags = tags; + } + public static class Builder { private PrioritizedExperiment instance; @@ -336,6 +353,7 @@ private Builder(Experiment other, Integer priority) { instance.isRapidExperiment = other.getIsRapidExperiment(); instance.userCap = other.getUserCap(); instance.creatorID = other.getCreatorID(); + instance.tags = other.getTags(); } private Date copyDate(Date date) { @@ -396,7 +414,6 @@ public Builder withModelVersion(String modelVersion) { public Builder withLabel(final Experiment.Label label) { instance.label = label; - return this; } @@ -415,6 +432,11 @@ public Builder withCreatorID(final String creatorID) { return this; } + public Builder withTags(final Set tags) { + instance.setTags(tags); + return this; + } + public PrioritizedExperiment build() { PrioritizedExperiment result = instance; instance = null; diff --git a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ContextTest.java b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ContextTest.java index f31ee91be..bfa59b31e 100644 --- a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ContextTest.java +++ b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ContextTest.java @@ -28,8 +28,6 @@ /** * This class tests the functionality of the Context object. - *

- * Created by asuckro on 8/12/15. */ public class ContextTest { diff --git a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentBatchTest.java b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentBatchTest.java index c0451c577..8afd10f3c 100644 --- a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentBatchTest.java +++ b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentBatchTest.java @@ -28,8 +28,6 @@ /** * This class is testing the functionality of the {@link ExperimentBatch} - *

- * Created by asuckro on 8/19/15. */ public class ExperimentBatchTest { diff --git a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentIDListTest.java b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentIDListTest.java index 0b5529eeb..876d3d095 100644 --- a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentIDListTest.java +++ b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentIDListTest.java @@ -26,8 +26,6 @@ /** * This class tests the functionality of the {@link ExperimentIDList} - *

- * Created by asuckro on 8/19/15. */ public class ExperimentIDListTest { diff --git a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentListTest.java b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentListTest.java index 7e56b7e63..c50c8356a 100644 --- a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentListTest.java +++ b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/ExperimentListTest.java @@ -24,8 +24,6 @@ /** * This tests the functionality of the {@link ExperimentList} - *

- * Created by asuckro on 8/20/15. */ public class ExperimentListTest { diff --git a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/NewExperimentTest.java b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/NewExperimentTest.java index 901cb20c1..e7faec88f 100644 --- a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/NewExperimentTest.java +++ b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/NewExperimentTest.java @@ -17,14 +17,17 @@ import org.junit.Test; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; +import java.util.Set; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; /** - * Created on 3/10/16. + * Test for the {@link NewExperiment} class. */ public class NewExperimentTest { @@ -32,6 +35,8 @@ public class NewExperimentTest { public void testBuilderCreation() { Experiment.ID id = Experiment.ID.newInstance(); Date date = new Date(); + String[] tagsArray = {"ui", "mobile", "12&"}; + Set tags = new HashSet<>(Arrays.asList(tagsArray)); NewExperiment.Builder builder = NewExperiment.withID(id); NewExperiment newExperiment = builder.withDescription("desc") .withIsPersonalizationEnabled(true) @@ -46,6 +51,7 @@ public void testBuilderCreation() { .withIsRapidExperiment(true) .withUserCap(1000) .withCreatorID("c1") + .withTags(tags) .build(); assertThat(newExperiment.getApplicationName(), is(Application.Name.valueOf("app"))); @@ -57,6 +63,10 @@ public void testBuilderCreation() { assertThat(newExperiment.getID(), is(id)); assertThat(newExperiment.getDescription(), is("desc")); assertThat(newExperiment.getLabel(), is(Experiment.Label.valueOf("label"))); + assertThat(newExperiment.getTags().size(), is(3)); + Arrays.sort(tagsArray); // the tags should be sorted + assertThat(newExperiment.getTags().toArray(), is(tagsArray)); + newExperiment.setApplicationName(Application.Name.valueOf("NewApp")); newExperiment.setCreatorID("c2"); @@ -72,7 +82,7 @@ public void testBuildBadSamplePercentage() { .withModelName("m1") .withModelVersion("v1") .withRule("r1") - .withSamplingPercent(null) //<-- this is what causes the execption + .withSamplingPercent(null) //<-- this is what causes the exception .withLabel(Experiment.Label.valueOf("label")) .withAppName(Application.Name.valueOf("app")) .withIsRapidExperiment(true) diff --git a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/PrioritizedExperimentTest.java b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/PrioritizedExperimentTest.java index 93cf272e5..b20479566 100644 --- a/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/PrioritizedExperimentTest.java +++ b/modules/experiment-objects/src/test/java/com/intuit/wasabi/experimentobjects/PrioritizedExperimentTest.java @@ -17,18 +17,23 @@ import org.junit.Test; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * Test class for the {@link PrioritizedExperiment} - *

- *

- * Created by asuckro on 8/12/15. */ public class PrioritizedExperimentTest { + private String[] tagsArray = {"ui", "mobile", "12&"}; + private Set tags = new HashSet<>(Arrays.asList(tagsArray)); private PrioritizedExperiment prioExp = PrioritizedExperiment.withID(Experiment.ID.newInstance()) .withApplicationName(Application.Name.valueOf("appName")) @@ -39,6 +44,7 @@ public class PrioritizedExperimentTest { .withPriority(3) .withUserCap(42) .withIsRapidExperiment(false) + .withTags(tags) .build(); private Experiment exp = Experiment.withID(Experiment.ID.newInstance()) @@ -49,6 +55,7 @@ public class PrioritizedExperimentTest { .withState(Experiment.State.RUNNING) .withIsRapidExperiment(true) .withUserCap(300) + .withTags(tags) .build(); @Test @@ -85,6 +92,7 @@ public void testBuilderFromOtherExperiment() { assertEquals(fromExp.getLabel(), exp.getLabel()); assertEquals(fromExp.getApplicationName(), exp.getApplicationName()); assertEquals(fromExp.getIsRapidExperiment(), exp.getIsRapidExperiment()); + assertEquals(fromExp.getTags(), exp.getTags()); } @Test @@ -96,14 +104,17 @@ public void testBuilderMethods() { .withModelVersion("1") .withCreatorID("c1") .withStartTime(startTime) - .withCreationTime(startTime).build(); + .withCreationTime(startTime) + .withTags(tags) + .build(); assertEquals(startTime, exp.getCreationTime()); assertEquals(startTime, exp.getStartTime()); assertEquals("m1", exp.getModelName()); assertEquals("1", exp.getModelVersion()); assertEquals("c1", exp.getCreatorID()); - + Arrays.sort(tagsArray); + assertArrayEquals(tagsArray, exp.getTags().toArray()); } @Test @@ -111,5 +122,19 @@ public void testClone() throws Exception { assertEquals(prioExp, prioExp.clone()); } + @Test + public void testIsDeleted() throws Exception { + assertFalse(prioExp.isDeleted()); + PrioritizedExperiment prioExp2 = prioExp.clone(); + prioExp2.setState(Experiment.State.DELETED); + assertTrue(prioExp2.isDeleted()); + } + + @Test + public void addNullTags() { + prioExp.setTags(null); + assertEquals(prioExp.getTags(), null); + } + } diff --git a/modules/experiment/src/main/java/com/intuit/wasabi/experiment/Experiments.java b/modules/experiment/src/main/java/com/intuit/wasabi/experiment/Experiments.java index a1193f5c2..12d69886c 100644 --- a/modules/experiment/src/main/java/com/intuit/wasabi/experiment/Experiments.java +++ b/modules/experiment/src/main/java/com/intuit/wasabi/experiment/Experiments.java @@ -22,7 +22,10 @@ import com.intuit.wasabi.experimentobjects.NewExperiment; import com.intuit.wasabi.experimentobjects.exceptions.InvalidExperimentStateTransitionException; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Interface to perform CRUD operations on experiment. In addition, it also @@ -155,4 +158,14 @@ boolean buildUpdatedExperiment(Experiment experiment, Experiment updates, Experi * @return a list of Application.Name types */ List getApplications(); + + /** + * Returns all tags that belong to the Applications specified in the given + * {@link com.intuit.wasabi.experimentobjects.Application.Name}s. + * + * @param applicationNames the {@link com.intuit.wasabi.experimentobjects.Application.Name}s for which the + * tags should be retrieved + * @return a {@link Map} containing the Applications and their tags + */ + Map> getTagsForApplications(Collection applicationNames); } diff --git a/modules/experiment/src/main/java/com/intuit/wasabi/experiment/impl/ExperimentsImpl.java b/modules/experiment/src/main/java/com/intuit/wasabi/experiment/impl/ExperimentsImpl.java index b594af70f..a031ffea3 100644 --- a/modules/experiment/src/main/java/com/intuit/wasabi/experiment/impl/ExperimentsImpl.java +++ b/modules/experiment/src/main/java/com/intuit/wasabi/experiment/impl/ExperimentsImpl.java @@ -42,8 +42,12 @@ import javax.inject.Inject; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.Set; import static com.intuit.wasabi.experimentobjects.Experiment.State.DELETED; import static com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT; @@ -105,6 +109,14 @@ public List getApplications() { return cassandraRepository.getApplicationsList(); } + /** + * {@inheritDoc} + */ + @Override + public Map> getTagsForApplications(Collection applicationNames) { + return cassandraRepository.getTagListForApplications(applicationNames != null ? applicationNames : Collections.emptySet()); + } + /** * {@inheritDoc} */ @@ -450,6 +462,16 @@ public boolean buildUpdatedExperiment(Experiment experiment, Experiment updates, changeList.add(changeData); } + if (updates.getTags() != null && !updates.getTags().equals(experiment.getTags())) { + builder.withTags(updates.getTags()); + requiresUpdate = true; + Set oldTags = experiment.getTags() == null ? Collections.EMPTY_SET : experiment.getTags(); + changeData = new ExperimentAuditInfo("tags", + oldTags.toString(), + updates.getTags().toString()); + changeList.add(changeData); + } + /* * Application name and label cannot be changed once the experiment is beyond the DRAFT state. * Hence, we are not including them as a part of the audit log. diff --git a/modules/experiment/src/test/java/com/intuit/wasabi/experiment/ExperimentsImplTest.java b/modules/experiment/src/test/java/com/intuit/wasabi/experiment/ExperimentsImplTest.java index c12aaed3b..d3468b52b 100644 --- a/modules/experiment/src/test/java/com/intuit/wasabi/experiment/ExperimentsImplTest.java +++ b/modules/experiment/src/test/java/com/intuit/wasabi/experiment/ExperimentsImplTest.java @@ -41,6 +41,7 @@ import org.mockito.runners.MockitoJUnitRunner; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -831,4 +832,11 @@ public void testBuildUpdateExperimentIsRapid() { then(changeList.size()).isEqualTo(1); then(result).isEqualTo(true); } + + @Test + public void testGetTags() { + when(cassandraRepository.getTagListForApplications(null)).thenReturn(null); + expImpl.getTagsForApplications(null); + verify(cassandraRepository).getTagListForApplications(Collections.emptySet()); + } } diff --git a/modules/functional-test/Vagrantfile b/modules/functional-test/Vagrantfile index 0d982588f..1f7278de9 100644 --- a/modules/functional-test/Vagrantfile +++ b/modules/functional-test/Vagrantfile @@ -13,8 +13,8 @@ Vagrant.configure("2") do |config| dev.vm.provider "virtualbox" do |v| v.name = "wasabi.os.it.vbox" - v.memory = 4096 - v.cpus = 4 + v.memory = 8192 + v.cpus = 8 v.customize ["modifyvm", :id, "--cableconnected1", "on"] end end diff --git a/modules/functional-test/provision.sh b/modules/functional-test/provision.sh index a54d05585..6645061aa 100755 --- a/modules/functional-test/provision.sh +++ b/modules/functional-test/provision.sh @@ -25,8 +25,9 @@ yum -y install httpd sudo service httpd start # install Oracle JDK -wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" "http://download.oracle.com/otn-pub/java/jdk/8u92-b14/jdk-8u92-linux-x64.rpm" -sudo yum -y localinstall jdk-8u92-linux-x64.rpm +wget --no-check-certificate -c --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.rpm + +sudo yum -y localinstall jdk-8u131-linux-x64.rpm cat < tags; + /** * The serialization strategy for comparisons and JSON serialization. */ @@ -172,7 +178,7 @@ public Experiment(String id) { */ public Experiment(String label, Application application, String startTime, String endTime, double samplingPercent) { - this(label, application, startTime, endTime, samplingPercent, null, null, false, "", "", false, 0, null); + this(label, application, startTime, endTime, samplingPercent, null, null, false, "", "", false, 0, null, null); } /** @@ -191,10 +197,12 @@ public Experiment(String label, Application application, String startTime, Strin * @param isRapidExperiment flag if rapid experimentation * @param userCap max users for rapid experiments * @param creatorID the creator + * @param tags the set of tags */ public Experiment(String label, Application application, String startTime, String endTime, double samplingPercent, String description, String rule, Boolean isPersonalizationEnabled, String modelName, - String modelVersion, Boolean isRapidExperiment, Integer userCap, String creatorID) { + String modelVersion, Boolean isRapidExperiment, Integer userCap, String creatorID, + Set tags) { this.setLabel(label) .setApplication(application) .setStartTime(startTime) @@ -207,7 +215,8 @@ public Experiment(String label, Application application, String startTime, Strin .setIsPersonalizationEnabled(isPersonalizationEnabled) .setIsRapidExperiment(isRapidExperiment) .setUserCap(userCap) - .setCreatorID(creatorID); + .setCreatorID(creatorID) + .setTags(tags); } /** @@ -518,6 +527,17 @@ public Experiment setCreatorID(String creatorID) { return this; } + /** + * Sets the tags for an experiment + * + * @param tags the tags + * @return this + */ + public Experiment setTags(Set tags) { + this.tags = tags; + return this; + } + @Override public void setSerializationStrategy(SerializationStrategy serializationStrategy) { Experiment.serializationStrategy = serializationStrategy; diff --git a/modules/main/src/main/env/docker/wasabi/Dockerfile b/modules/main/src/main/env/docker/wasabi/Dockerfile index 734cccbbd..ce1243b94 100644 --- a/modules/main/src/main/env/docker/wasabi/Dockerfile +++ b/modules/main/src/main/env/docker/wasabi/Dockerfile @@ -25,13 +25,13 @@ ENV WASABI_SRC_DIR ${application.name} ENV WASABI_HOME /usr/local/${application.name} ENV WASABI_JAVA_OPTIONS "" -ENV JDK_MAJOR_VERSION 8u92 -ENV JDK_MINOR_VERSION b14 +ENV JDK_MAJOR_VERSION 8u131 +ENV JDK_MINOR_VERSION b11 ENV JDK_VERSION ${JDK_MAJOR_VERSION}-${JDK_MINOR_VERSION} RUN yum -y update && yum install -y wget -RUN wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/${JDK_VERSION}/jdk-${JDK_MAJOR_VERSION}-linux-x64.rpm \ +RUN wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/${JDK_VERSION}/d54c1d3a095b4ff2b6607d096fa80163/jdk-${JDK_MAJOR_VERSION}-linux-x64.rpm \ && rpm -ivh jdk-${JDK_MAJOR_VERSION}-linux-x64.rpm && rm jdk-${JDK_MAJOR_VERSION}-linux-x64.rpm COPY ./ ${WASABI_HOME}/ diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/ExperimentRepository.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/ExperimentRepository.java index 89091ddec..a0eb4a83d 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/ExperimentRepository.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/ExperimentRepository.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; /** * Mid-level interface for the experiments repository @@ -265,5 +266,12 @@ void logBucketChanges(Experiment.ID experimentID, Bucket.Label bucketLabel, */ void createApplication(Application.Name applicationName); + /** + * Gets a list of tags associated with the given {@link Application.Name}. + * + * @param applicationNames the list of {@link Application.Name}s the tags should be retrieved for + * @return a Map of {@link Application.Name}s to their tags + */ + Map> getTagListForApplications(Collection applicationNames); } diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/CassandraRepositoryModule.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/CassandraRepositoryModule.java index f1803b0c6..486a31628 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/CassandraRepositoryModule.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/CassandraRepositoryModule.java @@ -35,6 +35,7 @@ import com.intuit.wasabi.repository.cassandra.accessor.ExclusionAccessor; import com.intuit.wasabi.repository.cassandra.accessor.ExperimentAccessor; import com.intuit.wasabi.repository.cassandra.accessor.ExperimentPageAccessor; +import com.intuit.wasabi.repository.cassandra.accessor.ExperimentTagAccessor; import com.intuit.wasabi.repository.cassandra.accessor.PrioritiesAccessor; import com.intuit.wasabi.repository.cassandra.accessor.StagingAccessor; import com.intuit.wasabi.repository.cassandra.accessor.UserFeedbackAccessor; @@ -65,6 +66,7 @@ import com.intuit.wasabi.repository.cassandra.provider.ExclusionAccessorProvider; import com.intuit.wasabi.repository.cassandra.provider.ExperimentAccessorProvider; import com.intuit.wasabi.repository.cassandra.provider.ExperimentPageAccessorProvider; +import com.intuit.wasabi.repository.cassandra.provider.ExperimentTagAccessorProvider; import com.intuit.wasabi.repository.cassandra.provider.MappingManagerProvider; import com.intuit.wasabi.repository.cassandra.provider.PrioritiesAccessorProvider; import com.intuit.wasabi.repository.cassandra.provider.StagingAccessorProvider; @@ -90,7 +92,6 @@ import static com.google.inject.name.Names.named; import static com.intuit.autumn.utils.PropertyFactory.create; import static com.intuit.autumn.utils.PropertyFactory.getProperty; -import static java.lang.Boolean.TRUE; import static java.lang.Integer.parseInt; import static org.slf4j.LoggerFactory.getLogger; @@ -137,6 +138,7 @@ protected void configure() { bind(UserFeedbackAccessor.class).toProvider(UserFeedbackAccessorProvider.class).in(Singleton.class); bind(UserInfoAccessor.class).toProvider(UserInfoAccessorProvider.class).in(Singleton.class); bind(UserRoleAccessor.class).toProvider(UserRoleAccessorProvider.class).in(Singleton.class); + bind(ExperimentTagAccessor.class).toProvider(ExperimentTagAccessorProvider.class).in(Singleton.class); //Bind those indexes bind(AppPageIndexAccessor.class).toProvider(AppPageIndexAccessorProvider.class).in(Singleton.class); diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/ClientConfiguration.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/ClientConfiguration.java index d66f57e76..736fbcdea 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/ClientConfiguration.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/ClientConfiguration.java @@ -16,6 +16,7 @@ package com.intuit.wasabi.repository.cassandra; import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.SocketOptions; import com.intuit.wasabi.cassandra.datastax.CassandraDriver; import java.util.Arrays; @@ -135,6 +136,16 @@ public int getPoolTimeoutMillis() { return parseInt(getProperty("poolTimeoutMillis", properties, "0")); } + @Override + public int getConnectTimeoutMillis() { + return parseInt(getProperty("connectTimeoutMillis", properties, String.valueOf(SocketOptions.DEFAULT_CONNECT_TIMEOUT_MILLIS))); + } + + @Override + public int getReadTimeoutMillis() { + return parseInt(getProperty("readTimeoutMillis", properties, String.valueOf(SocketOptions.DEFAULT_READ_TIMEOUT_MILLIS))); + } + @Override public int getMaxConnectionsPerHostRemote() { return parseInt(getProperty("maxConnectionsPerHostRemote", properties, "32")); diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessor.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessor.java index 1c4e24e5c..1762435fc 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessor.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessor.java @@ -21,9 +21,11 @@ import com.datastax.driver.mapping.annotations.Query; import com.google.common.util.concurrent.ListenableFuture; import com.intuit.wasabi.repository.cassandra.pojo.Experiment; +import com.intuit.wasabi.repository.cassandra.pojo.index.ExperimentTagsByApplication; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.UUID; /** @@ -53,13 +55,13 @@ public interface ExperimentAccessor { "rule = ?, sample_percent = ?, " + "start_time = ?, end_time = ?, " + "state=?, label=?, app_name=?, modified=? , is_personalized=?, model_name=?, model_version=?," + - " is_rapid_experiment=?, user_cap=?" + + " is_rapid_experiment=?, user_cap=?, tags=?" + " where id = ?") ResultSet updateExperiment(String description, String hypothesisIsCorrect, String results, String rule, double sample_percent, Date start_time, Date end_time, String state, String label, String app_name, Date modified, boolean is_personalized, String model_name, String model_version, - boolean is_rapid_experiment, int user_cap, UUID experimentId); + boolean is_rapid_experiment, int user_cap, Set tags, UUID experimentId); @Query("select * from experiment where app_name = ?") @@ -71,14 +73,19 @@ ResultSet updateExperiment(String description, String hypothesisIsCorrect, Strin @Query("select * from experiment where id = ?") Result selectBy(UUID experimentId); + @Query("insert into experiment " + "(id, description, hypothesis_is_correct, results, rule, sample_percent, start_time, end_time, " + " state, label, app_name, created, modified, is_personalized, model_name, model_version," + - " is_rapid_experiment, user_cap, creatorid) " + - "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + " is_rapid_experiment, user_cap, creatorid, tags) " + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") void insertExperiment(UUID experimentId, String description, String hypothesisIsCorrect, String results, String rule, double samplePercent, Date startTime, Date endTime, String state, String label, String appName, Date created, Date modified, boolean isPersonalized, String modelName, - String modelVersion, boolean isRapidExperiment, int userCap, String creatorid); + String modelVersion, boolean isRapidExperiment, int userCap, String creatorid, + Set tags); + + @Query("select tags from experiment where app_name = ?") + Result getAllTags(String appName); } diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentTagAccessor.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentTagAccessor.java new file mode 100644 index 000000000..a88707ec5 --- /dev/null +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentTagAccessor.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright 2017 Intuit + *

+ * Licensed 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 com.intuit.wasabi.repository.cassandra.accessor; + +import com.datastax.driver.mapping.Result; +import com.datastax.driver.mapping.annotations.Accessor; +import com.datastax.driver.mapping.annotations.Query; +import com.google.common.util.concurrent.ListenableFuture; +import com.intuit.wasabi.repository.cassandra.pojo.index.ExperimentTagsByApplication; + +import java.util.Set; +import java.util.UUID; + +/** + * Accessor for the tags associated with Experiments. + */ +@Accessor +public interface ExperimentTagAccessor { + + @Query("select * from experiment_tag where app_name = ?") + ListenableFuture> getExperimentTagsAsync(String appName); + + @Query("insert into experiment_tag(app_name, exp_id, tags) values (?,?,?)") + void insert(String appName, UUID experimentId, Set tags); + +} diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepository.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepository.java index 8081a8440..df2dfa876 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepository.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepository.java @@ -45,22 +45,27 @@ import com.intuit.wasabi.repository.cassandra.accessor.ApplicationListAccessor; import com.intuit.wasabi.repository.cassandra.accessor.BucketAccessor; import com.intuit.wasabi.repository.cassandra.accessor.ExperimentAccessor; +import com.intuit.wasabi.repository.cassandra.accessor.ExperimentTagAccessor; import com.intuit.wasabi.repository.cassandra.accessor.audit.BucketAuditLogAccessor; import com.intuit.wasabi.repository.cassandra.accessor.audit.ExperimentAuditLogAccessor; import com.intuit.wasabi.repository.cassandra.accessor.index.ExperimentLabelIndexAccessor; import com.intuit.wasabi.repository.cassandra.accessor.index.ExperimentState; import com.intuit.wasabi.repository.cassandra.accessor.index.StateExperimentIndexAccessor; import com.intuit.wasabi.repository.cassandra.pojo.index.ExperimentByAppNameLabel; +import com.intuit.wasabi.repository.cassandra.pojo.index.ExperimentTagsByApplication; import com.intuit.wasabi.repository.cassandra.pojo.index.StateExperimentIndex; import org.slf4j.Logger; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import static org.slf4j.LoggerFactory.getLogger; @@ -90,6 +95,8 @@ public class CassandraExperimentRepository implements ExperimentRepository { private ExperimentAuditLogAccessor experimentAuditLogAccessor; + private ExperimentTagAccessor experimentTagAccessor; + /** * @return the experimentAccessor */ @@ -207,7 +214,8 @@ public CassandraExperimentRepository(CassandraDriver driver, BucketAuditLogAccessor bucketAuditLogAccessor, ExperimentAuditLogAccessor experimentAuditLogAccessor, StateExperimentIndexAccessor stateExperimentIndexAccessor, - ExperimentValidator validator) { + ExperimentValidator validator, + ExperimentTagAccessor experimentTagAccessor) { this.driver = driver; this.experimentAccessor = experimentAccessor; this.experimentLabelIndexAccessor = experimentLabelIndexAccessor; @@ -217,6 +225,7 @@ public CassandraExperimentRepository(CassandraDriver driver, this.bucketAuditLogAccessor = bucketAuditLogAccessor; this.experimentAuditLogAccessor = experimentAuditLogAccessor; this.validator = validator; + this.experimentTagAccessor = experimentTagAccessor; } /** @@ -389,10 +398,16 @@ public Experiment.ID createExperiment(NewExperiment newExperiment) { newExperiment.getModelVersion(), newExperiment.getIsRapidExperiment(), newExperiment.getUserCap(), - (newExperiment.getCreatorID() != null) ? newExperiment.getCreatorID() : ""); + (newExperiment.getCreatorID() != null) ? newExperiment.getCreatorID() : "", + newExperiment.getTags()); + // TODO - Do we need to create an application while creating a new experiment ? createApplication(newExperiment.getApplicationName()); + + // update tags + updateExperimentTags(newExperiment.getApplicationName(), newExperiment.getId(), newExperiment.getTags()); + } catch (Exception e) { LOGGER.error("Error while creating experiment {}", newExperiment, e); throw new RepositoryException("Exception while creating experiment " + newExperiment + " message " + e, e); @@ -401,6 +416,24 @@ public Experiment.ID createExperiment(NewExperiment newExperiment) { } + /** + * Updates the tags for a given Experiment. This also means that tags are deleted from + * the lookup table in Cassandra if they are removed from the experiment. + * + * @param applicationName the Application for which the tags should be added + * @param expId the Id of the experiment that has changed tags + * @param tags list of tags for the experiment + */ + public void updateExperimentTags(Application.Name applicationName, Experiment.ID expId, Set tags) { + + if (tags == null) + tags = Collections.EMPTY_SET; // need this to update this to delete tags + + // update the tag table, just rewrite the complete entry + experimentTagAccessor.insert(applicationName.toString(), expId.getRawID(), tags); + } + + /** * {@inheritDoc} */ @@ -519,8 +552,11 @@ public Experiment updateExperiment(Experiment experiment) { experiment.getModelVersion(), experiment.getIsRapidExperiment(), experiment.getUserCap(), + experiment.getTags(), experiment.getID().getRawID()); + updateExperimentTags(experiment.getApplicationName(), experiment.getID(), experiment.getTags()); + // Point the experiment index to this experiment updateExperimentLabelIndex(experiment.getID(), experiment.getApplicationName(), experiment.getLabel(), experiment.getStartTime(), experiment.getEndTime(), experiment.getState()); @@ -1194,4 +1230,40 @@ public void createApplication(Application.Name applicationName) { } } + /** + * {@inheritDoc} + */ + @Override + public Map> getTagListForApplications(Collection applicationNames) { + + LOGGER.debug("Retrieving Experiment Tags for applications {}", applicationNames); + + try { + List>> futures = new ArrayList<>(); + + for (Application.Name appName : applicationNames) { + ListenableFuture> resultSetFuture = experimentTagAccessor + .getExperimentTagsAsync(appName.toString()); + futures.add(resultSetFuture); + } + + Map> result = new HashMap<>(); + for (ListenableFuture> future : futures) { + List expTagApplication = future.get().all(); + + Set allTagsForApplication = new TreeSet<>(); + for (ExperimentTagsByApplication expTagsByApp : expTagApplication) { + allTagsForApplication.addAll(expTagsByApp.getTags()); + } + result.put(Application.Name.valueOf(expTagApplication.get(0).getAppName()), allTagsForApplication); + } + + return result; + } catch (Exception e) { + LOGGER.error("Error while retieving ExperimentTags for {}", applicationNames, e); + throw new RepositoryException("Unable to get ExperimentTags for applications: \"" + + applicationNames.toString() + "\"" + e); + } + } + } diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/ExperimentHelper.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/ExperimentHelper.java index fef9bed37..604ce621a 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/ExperimentHelper.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/impl/ExperimentHelper.java @@ -83,6 +83,7 @@ public static Experiment makeExperiment( experimentObject.setRuleJson(convertRuleToJson(experimentPojo.getRule())); experimentObject.setHypothesisIsCorrect(experimentPojo.getHypothesisIsCorrect()); experimentObject.setResults(experimentPojo.getResults()); + experimentObject.setTags(experimentPojo.getTags()); return experimentObject; } diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/Experiment.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/Experiment.java index 7c53014c2..245a6244a 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/Experiment.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/Experiment.java @@ -24,6 +24,7 @@ import lombok.NoArgsConstructor; import java.util.Date; +import java.util.Set; import java.util.UUID; /***** @@ -85,6 +86,9 @@ public class Experiment { @Column(name = "creatorid") private String creatorId; + @Column(name = "tags") + private Set tags; + @Column(name = "hypothesis_is_correct") private String hypothesisIsCorrect; diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/index/ExperimentTagsByApplication.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/index/ExperimentTagsByApplication.java new file mode 100644 index 000000000..33972186d --- /dev/null +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/pojo/index/ExperimentTagsByApplication.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright 2017 Intuit + *

+ * Licensed 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 com.intuit.wasabi.repository.cassandra.pojo.index; + +import com.datastax.driver.mapping.annotations.ClusteringColumn; +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.PartitionKey; +import com.datastax.driver.mapping.annotations.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +@Table(name = "experiment_tag") +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class ExperimentTagsByApplication { + + @PartitionKey(0) + @Column(name = "app_name") + String appName; + + @ClusteringColumn(0) + @Column(name = "exp_id") + UUID expId; + + @Column(name = "tags") + Set tags; + + public Set getTags() { + return tags != null ? tags : Collections.EMPTY_SET; + } +} diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/provider/ExperimentTagAccessorProvider.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/provider/ExperimentTagAccessorProvider.java new file mode 100644 index 000000000..b7ab01d86 --- /dev/null +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/cassandra/provider/ExperimentTagAccessorProvider.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright 2017 Intuit + *

+ * Licensed 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 com.intuit.wasabi.repository.cassandra.provider; + +import com.datastax.driver.core.Session; +import com.datastax.driver.mapping.MappingManager; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.intuit.wasabi.cassandra.datastax.CassandraDriver; +import com.intuit.wasabi.repository.cassandra.accessor.ExperimentTagAccessor; + + +public class ExperimentTagAccessorProvider implements Provider { + private final Session session; + private final MappingManager manager; + + @Inject + public ExperimentTagAccessorProvider(CassandraDriver driver) { + this.session = driver.getSession(); + this.manager = new MappingManager(session); + } + + + @Override + public ExperimentTagAccessor get() { + return manager.createAccessor(ExperimentTagAccessor.class); + } +} \ No newline at end of file diff --git a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepository.java b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepository.java index bcb7ad6db..559f7bc19 100644 --- a/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepository.java +++ b/modules/repository-datastax/src/main/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepository.java @@ -41,6 +41,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static com.intuit.wasabi.experimentobjects.Experiment.State.DELETED; @@ -745,9 +746,7 @@ public BucketList getBuckets(Experiment.ID experimentID, boolean checkExperiment } /** - * Creates an application at top level - * - * @param applicationName Application Name + * {@inheritDoc} */ @Override public void createApplication(Application.Name applicationName) { @@ -755,9 +754,13 @@ public void createApplication(Application.Name applicationName) { } @Override - public void updateStateIndex(Experiment experiment) { + public Map> getTagListForApplications(Collection applicationNames) { throw new UnsupportedOperationException("Not supported "); } + @Override + public void updateStateIndex(Experiment experiment) { + throw new UnsupportedOperationException("Not supported "); + } } diff --git a/modules/repository-datastax/src/main/resources/cassandra_client_config.properties b/modules/repository-datastax/src/main/resources/cassandra_client_config.properties index c1cfbc1bd..d79ff5873 100644 --- a/modules/repository-datastax/src/main/resources/cassandra_client_config.properties +++ b/modules/repository-datastax/src/main/resources/cassandra_client_config.properties @@ -25,6 +25,16 @@ keyspaceName:${cassandra.experiments.keyspaceName} isSlowQueryLoggingEnabled:False slowQueryLoggingThresholdMilli:100 +#The connection timeout in milliseconds. +#As the name implies, the connection timeout defines how long the driver waits to establish a new connection to a Cassandra node before giving up. +#Default value is 5000ms, increasing to 1 minutes to make functional tests run even in very low resource containers/environments. +connectTimeoutMillis:60000 + +#The per-host read timeout in milliseconds. +#This defines how long the driver will wait for a given Cassandra node to answer a query. +#Default value is 12000ms, increasing to 2 minutes to make functional tests run even in very low resource containers/environments. +readTimeoutMillis:120000 + #Both token aware balancing option is required. Otherwise will default to round robin policy #if the LocalDC is specified but the UsedHostsPerRemoteDc < 0 then we will use round robin policy tokenAwareLoadBalancingLocalDC:${cassandra.local.dc} diff --git a/modules/repository-datastax/src/main/resources/com/intuit/wasabi/repository/impl/cassandra/migration/V035__Create_ExperimentTag_table.cql b/modules/repository-datastax/src/main/resources/com/intuit/wasabi/repository/impl/cassandra/migration/V035__Create_ExperimentTag_table.cql new file mode 100644 index 000000000..59c8f082b --- /dev/null +++ b/modules/repository-datastax/src/main/resources/com/intuit/wasabi/repository/impl/cassandra/migration/V035__Create_ExperimentTag_table.cql @@ -0,0 +1,6 @@ +create table experiment_tag ( + app_name text, + exp_id uuid, + tags set, + PRIMARY KEY (app_name, exp_id) +); \ No newline at end of file diff --git a/modules/repository-datastax/src/main/resources/com/intuit/wasabi/repository/impl/cassandra/migration/V036__Alter_Experiment_Add_Tags.cql b/modules/repository-datastax/src/main/resources/com/intuit/wasabi/repository/impl/cassandra/migration/V036__Alter_Experiment_Add_Tags.cql new file mode 100644 index 000000000..8c85991f6 --- /dev/null +++ b/modules/repository-datastax/src/main/resources/com/intuit/wasabi/repository/impl/cassandra/migration/V036__Alter_Experiment_Add_Tags.cql @@ -0,0 +1 @@ +alter table experiment ADD tags set; \ No newline at end of file diff --git a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessorITest.java b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessorITest.java index 5786ae991..6162e8933 100644 --- a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessorITest.java +++ b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/accessor/ExperimentAccessorITest.java @@ -49,7 +49,7 @@ public void insertOneExperiments() { "d1", "yes", "r1", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l1", "app1", date1, date2, true, - "m1", "v1", true, 5000, "c1"); + "m1", "v1", true, 5000, "c1", null); Result experiment1 = accessor.getExperimentById(experimentId1); List experimentResult = experiment1.all(); @@ -80,12 +80,12 @@ public void insertTwoExperiments() { "d1", "yes", "r1", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l1", "app1", date1, date2, true, - "m1", "v1", true, 5000, "c1"); + "m1", "v1", true, 5000, "c1", null); accessor.insertExperiment(experimentId2, "d2", "no", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", "app2", date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); List experimentIds = new ArrayList<>(); diff --git a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryITest.java b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryITest.java index f3596e1d5..e64442be3 100644 --- a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryITest.java +++ b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryITest.java @@ -40,11 +40,14 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import static org.junit.Assert.assertEquals; @@ -114,6 +117,7 @@ public void setUp() throws Exception { newExperiment1.setStartTime(new Date()); newExperiment1.setEndTime(new Date()); newExperiment1.setSamplingPercent(0.2); + newExperiment1.setTags(new HashSet<>(Arrays.asList("tag1", "tag2"))); experimentID1 = repository.createExperiment(newExperiment1); repository.createIndicesForNewExperiment(newExperiment1); @@ -126,6 +130,7 @@ public void setUp() throws Exception { newExperiment2.setStartTime(new Date()); newExperiment2.setEndTime(new Date()); newExperiment2.setSamplingPercent(0.2); + newExperiment2.setTags(new HashSet<>(Arrays.asList("tag3", "tag4"))); experimentID2 = repository.createExperiment(newExperiment2); repository.createIndicesForNewExperiment(newExperiment2); @@ -788,4 +793,17 @@ public void testCreateOneBucketsAndDeleteOneBucket() { public void testCreateExperimentDuplicateThrowsException() { ID experimentId = repository.createExperiment(newExperiment1); } + + @Test + public void testAddTagsToExperiment() { + Experiment experiment = repository.getExperiment(experimentID1); + experiment.setTags(new HashSet<>(Arrays.asList("tagNew", "tag2"))); + repository.updateExperiment(experiment); + + List allTags = Arrays.asList("tag2", "tag3", "tag4", "tagNew"); + + Map> result = repository.getTagListForApplications(Arrays.asList(appname)); + assertTrue(result.size() == 1); // only one application + assertTrue(result.get(appname).containsAll(allTags)); + } } diff --git a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryTest.java b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryTest.java index 5066fa6e4..e84cae96b 100644 --- a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryTest.java +++ b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraExperimentRepositoryTest.java @@ -36,29 +36,35 @@ import com.intuit.wasabi.repository.cassandra.accessor.ApplicationListAccessor; import com.intuit.wasabi.repository.cassandra.accessor.BucketAccessor; import com.intuit.wasabi.repository.cassandra.accessor.ExperimentAccessor; +import com.intuit.wasabi.repository.cassandra.accessor.ExperimentTagAccessor; import com.intuit.wasabi.repository.cassandra.accessor.audit.BucketAuditLogAccessor; import com.intuit.wasabi.repository.cassandra.accessor.audit.ExperimentAuditLogAccessor; import com.intuit.wasabi.repository.cassandra.accessor.index.ExperimentLabelIndexAccessor; import com.intuit.wasabi.repository.cassandra.accessor.index.StateExperimentIndexAccessor; +import com.intuit.wasabi.repository.cassandra.pojo.index.ExperimentTagsByApplication; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.ExecutionException; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class CassandraExperimentRepositoryTest { @@ -86,6 +92,7 @@ public class CassandraExperimentRepositoryTest { private ExperimentAccessor mockExperimentAccessor; private ExperimentAuditLogAccessor mockExperimentAuditLogAccessor; + private ExperimentTagAccessor mockExperimentTagAccessor; private StateExperimentIndexAccessor mockStateExperimentIndexAccessor; @@ -102,6 +109,7 @@ public void setUp() throws Exception { mockExperimentAuditLogAccessor = Mockito.mock(ExperimentAuditLogAccessor.class); mockApplicationListAccessor = Mockito.mock(ApplicationListAccessor.class); mockExperimentLabelIndexAccessor = Mockito.mock(ExperimentLabelIndexAccessor.class); + mockExperimentTagAccessor = Mockito.mock(ExperimentTagAccessor.class); if (repository != null) return; @@ -114,7 +122,8 @@ public void setUp() throws Exception { mockDriver, mockExperimentAccessor, mockExperimentLabelIndexAccessor, mockBucketAccessor, mockApplicationListAccessor, mockBucketAuditLogAccessor, mockExperimentAuditLogAccessor, - mockStateExperimentIndexAccessor, new ExperimentValidator()); + mockStateExperimentIndexAccessor, new ExperimentValidator(), + mockExperimentTagAccessor); bucket1 = Bucket.newInstance(experimentID1, Bucket.Label.valueOf("bl1")).withAllocationPercent(.23) .withControl(true) .withDescription("b1").withPayload("p1") @@ -641,4 +650,74 @@ public void testGetBucketList() throws ExecutionException, InterruptedException assertThat(actualResultMap.get(expId1).getBuckets().get(0).getLabel().toString(), is("Test-Bucket-1")); } + @Test + public void testUpdateTags() { + //------ Input -------- + Application.Name appName = Application.Name.valueOf("AppName"); + + Set exp1 = new TreeSet<>(Arrays.asList("tag1", "tag3")); + Set exp2 = new TreeSet<>(Arrays.asList("tag4")); + ExperimentTagsByApplication exp2Tags = ExperimentTagsByApplication.builder() + .tags(exp2).appName(appName.toString()).build(); + ExperimentTagsByApplication exp1Tags = ExperimentTagsByApplication.builder() + .tags(exp1).appName(appName.toString()).build(); + + List dbResultList = Arrays.asList(exp1Tags, exp2Tags); + + //------ Mocking interacting calls + Result dbResult = mock(Result.class); + when(mockExperimentAccessor.getAllTags(appName.toString())).thenReturn(dbResult); + when(dbResult.all()).thenReturn(dbResultList); + + //------ Make call + repository.updateExperimentTags(appName, experimentID1, exp1); + repository.updateExperimentTags(appName, experimentID2, exp2); + + //------ Verify result + verify(mockExperimentTagAccessor).insert(appName.toString(), experimentID1.getRawID(), + new TreeSet<>(Arrays.asList("tag1", "tag3"))); + verify(mockExperimentTagAccessor).insert(appName.toString(), experimentID2.getRawID(), + new TreeSet<>(Arrays.asList("tag4"))); + } + + @Test + public void testGetTagList() throws ExecutionException, InterruptedException { + //------ Input -------- + Application.Name app01 = Application.Name.valueOf("app01"); + Application.Name app02 = Application.Name.valueOf("app02"); + List appNames = Arrays.asList(app01, app02); + + Set exp1 = new TreeSet<>(Arrays.asList("tag1", "tag3")); + Set exp2 = new TreeSet<>(Arrays.asList("tag4")); + + List exp1Tags = Arrays.asList(ExperimentTagsByApplication.builder() + .tags(exp1).appName(app01.toString()).build()); + List exp2Tags = Arrays.asList(ExperimentTagsByApplication.builder() + .tags(exp2).appName(app02.toString()).build()); + + //------ Mocking interacting calls + ListenableFuture> dbResultFuture1 = mock(ListenableFuture.class); + ListenableFuture> dbResultFuture2 = mock(ListenableFuture.class); + + Result dbResult1 = mock(Result.class); + Result dbResult2 = mock(Result.class); + + when(mockExperimentTagAccessor.getExperimentTagsAsync("app01")).thenReturn(dbResultFuture1); + when(mockExperimentTagAccessor.getExperimentTagsAsync("app02")).thenReturn(dbResultFuture2); + + when(dbResultFuture1.get()).thenReturn(dbResult1); + when(dbResultFuture2.get()).thenReturn(dbResult2); + + when(dbResult1.all()).thenReturn(exp1Tags); + when(dbResult2.all()).thenReturn(exp2Tags); + + //------ Make call + Map> callResult = repository.getTagListForApplications(appNames); + + //------ Verify result + assertEquals(2, callResult.size()); + assertArrayEquals(exp1.toArray(), callResult.get(app01).toArray()); + assertArrayEquals(exp2.toArray(), callResult.get(app02).toArray()); + } + } diff --git a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraMutexRepositoryITest.java b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraMutexRepositoryITest.java index ce0ecda38..2ebd4c022 100644 --- a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraMutexRepositoryITest.java +++ b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraMutexRepositoryITest.java @@ -78,12 +78,12 @@ public void testGetExclusionsSuccess() { "d1", "yes", "r1", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l1", "app1", date1, date2, true, - "m1", "v1", true, 5000, "c1"); + "m1", "v1", true, 5000, "c1", null); experimentAccessor.insertExperiment(pair.getRawID(), "d2", "no", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", "app2", date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); repository.createExclusion(base, pair); @@ -107,17 +107,17 @@ public void testGetExclusionsWithTwoExclusiosSuccess() { "d1", "yes", "r1", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l1", "app1", date1, date2, true, - "m1", "v1", true, 5000, "c1"); + "m1", "v1", true, 5000, "c1", null); experimentAccessor.insertExperiment(pair1.getRawID(), "d2", "yes", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", "app2", date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); experimentAccessor.insertExperiment(pair2.getRawID(), "d2", "yes", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", "app2", date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); repository.createExclusion(base, pair1); repository.createExclusion(base, pair2); @@ -154,19 +154,19 @@ public void testGetNotExclusionsSuccess() { "d1", "yes", "r1", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l1", appName, date1, date2, true, - "m1", "v1", true, 5000, "c1"); + "m1", "v1", true, 5000, "c1", null); experimentAccessor.insertExperiment(pair1.getRawID(), "d2", "yes", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", appName, date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); experimentAccessor.insertExperiment(notExclusion.getRawID(), "d2", "yes", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", appName, date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); repository.createExclusion(base, pair1); diff --git a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraPrioritiesRepositoryITest.java b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraPrioritiesRepositoryITest.java index a999674d6..55cd34bab 100644 --- a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraPrioritiesRepositoryITest.java +++ b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/cassandra/impl/CassandraPrioritiesRepositoryITest.java @@ -83,13 +83,13 @@ public void testTwoGetPrioritiesSuccess() { "d1", "yes", "r1", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l1", applicationName.toString(), date1, date2, true, - "m1", "v1", true, 5000, "c1"); + "m1", "v1", true, 5000, "c1", null); experimentAccessor.insertExperiment(experimentId2, "d2", "yes", "r2", "", 1.0, date1, date2, com.intuit.wasabi.experimentobjects.Experiment.State.DRAFT.name(), "l2", applicationName.toString(), date1, date2, true, - "m2", "v2", true, 5000, "c2"); + "m2", "v2", true, 5000, "c2", null); List priorityIds = new ArrayList<>(); priorityIds.add(experimentId1); diff --git a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepositoryTest.java b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepositoryTest.java index d67453eab..00b4bc010 100644 --- a/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepositoryTest.java +++ b/modules/repository-datastax/src/test/java/com/intuit/wasabi/repository/database/DatabaseExperimentRepositoryTest.java @@ -518,4 +518,14 @@ public void testGetBuckets() { assertThat(result.getBuckets().size(), is(1)); assertThat(result.getBuckets().get(0).getExperimentID(), is(id)); } + + @Test(expected = UnsupportedOperationException.class) + public void testUnsupportedTagMethod() { + repository.getTagListForApplications(Collections.EMPTY_LIST); + } + + @Test(expected = UnsupportedOperationException.class) + public void testUnsupportedApplicationMethod() { + repository.createApplication(Application.Name.valueOf("NotSupported")); + } } diff --git a/modules/ui/README.md b/modules/ui/README.md index ef3c0cd62..ef736d552 100644 --- a/modules/ui/README.md +++ b/modules/ui/README.md @@ -57,3 +57,21 @@ Then: This will build the UI into the dist folder and then start a web server, serving the UI from that folder on http://localhost:9000 . + +### Troubleshooting + +If you happen to get some errors or problems with building a new version of the Wasabi UI after an update, you should +follow these steps to refresh the build-time and run-time libraries: + +``` +% cd modules/ui +% rm -rf node_modules +% rm -rf app/bower_components +% npm install +% bower install +``` + +This will ensure that the versions of all the libraries are consistent with the current version of the Wasabi +UI code. + + diff --git a/modules/ui/app/images/icon_filter.png b/modules/ui/app/images/icon_filter.png new file mode 100644 index 000000000..798899548 Binary files /dev/null and b/modules/ui/app/images/icon_filter.png differ diff --git a/modules/ui/app/images/icon_filter_empty.png b/modules/ui/app/images/icon_filter_empty.png new file mode 100644 index 000000000..170ddd35d Binary files /dev/null and b/modules/ui/app/images/icon_filter_empty.png differ diff --git a/modules/ui/app/index.html b/modules/ui/app/index.html index cd426c0f6..ef00788e7 100644 --- a/modules/ui/app/index.html +++ b/modules/ui/app/index.html @@ -14,7 +14,9 @@ + + @@ -44,9 +46,9 @@