/* * Copyright (c) 2016 Couchbase, Inc. * * 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.couchbase.client.core.cluster; import com.couchbase.client.core.message.ResponseStatus; import com.couchbase.client.core.message.kv.AppendRequest; import com.couchbase.client.core.message.kv.AppendResponse; import com.couchbase.client.core.message.kv.CounterRequest; import com.couchbase.client.core.message.kv.CounterResponse; import com.couchbase.client.core.message.kv.GetRequest; import com.couchbase.client.core.message.kv.GetResponse; import com.couchbase.client.core.message.kv.InsertRequest; import com.couchbase.client.core.message.kv.InsertResponse; import com.couchbase.client.core.message.kv.MutationToken; import com.couchbase.client.core.message.kv.RemoveRequest; import com.couchbase.client.core.message.kv.RemoveResponse; import com.couchbase.client.core.message.kv.ReplaceRequest; import com.couchbase.client.core.message.kv.ReplaceResponse; import com.couchbase.client.core.message.kv.TouchRequest; import com.couchbase.client.core.message.kv.TouchResponse; import com.couchbase.client.core.message.kv.UnlockRequest; import com.couchbase.client.core.message.kv.UnlockResponse; import com.couchbase.client.core.message.kv.UpsertRequest; import com.couchbase.client.core.message.kv.UpsertResponse; import com.couchbase.client.core.util.ClusterDependentTest; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import org.junit.BeforeClass; import org.junit.Test; import rx.Observable; import rx.functions.Func1; import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** * Verifies basic functionality of binary operations. * * @author Michael Nitschinger * @since 1.0 */ public class KeyValueMessageTest extends ClusterDependentTest { @BeforeClass public static void setup() throws Exception { connect(false); } @Test public void shouldUpsertAndGetDocument() throws Exception { String key = "upsert-key"; String content = "Hello World!"; UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); UpsertResponse response = cluster().<UpsertResponse>send(upsert).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertValidMetadata(response.mutationToken()); GetRequest request = new GetRequest(key, bucket()); GetResponse getResponse = cluster().<GetResponse>send(request).toBlocking().single(); assertEquals(content, getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test public void shouldUpsertWithExpiration() throws Exception { String key = "upsert-key-vanish"; String content = "Hello World!"; UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), 1, 0, bucket()); UpsertResponse response = cluster().<UpsertResponse>send(upsert).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertValidMetadata(response.mutationToken()); Thread.sleep(2000); GetRequest request = new GetRequest(key, bucket()); GetResponse getResponse = cluster().<GetResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.NOT_EXISTS, getResponse.status()); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test public void shouldHandleDoubleInsert() throws Exception { String key = "insert-key"; String content = "Hello World!"; InsertRequest insert = new InsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); InsertResponse insertResponse = cluster().<InsertResponse>send(insert).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, insertResponse.status()); ReferenceCountUtil.releaseLater(insertResponse.content()); assertValidMetadata(insertResponse.mutationToken()); insert = new InsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); insertResponse = cluster().<InsertResponse>send(insert).toBlocking().single(); assertEquals(ResponseStatus.EXISTS, insertResponse.status()); ReferenceCountUtil.releaseLater(insertResponse.content()); assertNull(insertResponse.mutationToken()); } @Test public void shouldReplaceWithoutCAS() throws Exception { final String key = "replace-key"; final String content = "replace content"; ReplaceRequest insert = new ReplaceRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); ReplaceResponse response = cluster().<ReplaceResponse>send(insert).toBlocking().single(); assertEquals(ResponseStatus.NOT_EXISTS, response.status()); ReferenceCountUtil.releaseLater(response.content()); assertNull(response.mutationToken()); UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer("insert content", CharsetUtil.UTF_8), bucket()); response = cluster() .<UpsertResponse>send(upsert) .flatMap(new Func1<UpsertResponse, Observable<ReplaceResponse>>() { @Override public Observable<ReplaceResponse> call(UpsertResponse response) { ReferenceCountUtil.releaseLater(response.content()); return cluster().send(new ReplaceRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket())); } }).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertValidMetadata(response.mutationToken()); assertEquals(ResponseStatus.SUCCESS, response.status()); } @Test public void shouldReplaceWithFailingCAS() { final String key = "replace-key-cas-fail"; final String content = "replace content"; ReplaceRequest insert = new ReplaceRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); ReplaceResponse response = cluster().<ReplaceResponse>send(insert).toBlocking().single(); assertEquals(ResponseStatus.NOT_EXISTS, response.status()); ReferenceCountUtil.releaseLater(response.content()); assertNull(response.mutationToken()); UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer("insert content", CharsetUtil.UTF_8), bucket()); response = cluster().<UpsertResponse>send(upsert) .flatMap(new Func1<UpsertResponse, Observable<ReplaceResponse>>() { @Override public Observable<ReplaceResponse> call(UpsertResponse response) { ReferenceCountUtil.releaseLater(response.content()); return cluster().send(new ReplaceRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), 24234234L, bucket())); } }).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertEquals(ResponseStatus.EXISTS, response.status()); assertNull(response.mutationToken()); } @Test public void shouldReplaceWithMatchingCAS() throws Exception { final String key = "replace-key-cas-match"; final String content = "replace content"; ReplaceRequest insert = new ReplaceRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); ReplaceResponse response = cluster().<ReplaceResponse>send(insert).toBlocking().single(); assertEquals(ResponseStatus.NOT_EXISTS, response.status()); ReferenceCountUtil.releaseLater(response.content()); assertNull(response.mutationToken()); UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer("insert content", CharsetUtil.UTF_8), bucket()); response = cluster().<UpsertResponse>send(upsert) .flatMap(new Func1<UpsertResponse, Observable<ReplaceResponse>>() { @Override public Observable<ReplaceResponse> call(UpsertResponse response) { ReferenceCountUtil.releaseLater(response.content()); return cluster().send(new ReplaceRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), response.cas(), bucket())); } }).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertEquals(ResponseStatus.SUCCESS, response.status()); assertValidMetadata(response.mutationToken()); } @Test public void shouldRemoveDocumentWithoutCAS() throws Exception { String key = "remove-key"; String content = "Hello World!"; UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); UpsertResponse upsertResponse = cluster().<UpsertResponse>send(upsert).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, upsertResponse.status()); ReferenceCountUtil.releaseLater(upsertResponse.content()); assertValidMetadata(upsertResponse.mutationToken()); RemoveRequest remove = new RemoveRequest(key, bucket()); RemoveResponse response = cluster().<RemoveResponse>send(remove).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertTrue(response.cas() != 0); ReferenceCountUtil.releaseLater(response.content()); assertValidMetadata(response.mutationToken()); assertMetadataSequence(upsertResponse.mutationToken(), response.mutationToken()); GetRequest get = new GetRequest(key, bucket()); GetResponse getResponse = cluster().<GetResponse>send(get).toBlocking().single(); assertEquals(ResponseStatus.NOT_EXISTS, getResponse.status()); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test public void shouldRemoveDocumentWithCAS() throws Exception { String key = "remove-key-cas"; String content = "Hello World!"; UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); UpsertResponse upsertResponse = cluster().<UpsertResponse>send(upsert).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, upsertResponse.status()); ReferenceCountUtil.releaseLater(upsertResponse.content()); assertValidMetadata(upsertResponse.mutationToken()); RemoveRequest remove = new RemoveRequest(key, 1233443, bucket()); RemoveResponse response = cluster().<RemoveResponse>send(remove).toBlocking().single(); assertEquals(ResponseStatus.EXISTS, response.status()); ReferenceCountUtil.releaseLater(response.content()); assertNull(response.mutationToken()); remove = new RemoveRequest(key, upsertResponse.cas(), bucket()); response = cluster().<RemoveResponse>send(remove).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertTrue(response.cas() != 0); ReferenceCountUtil.releaseLater(response.content()); assertValidMetadata(response.mutationToken()); assertMetadataSequence(upsertResponse.mutationToken(), response.mutationToken()); } @Test public void shouldIncrementFromCounter() throws Exception { String key = "counter-incr"; CounterResponse response1 = cluster().<CounterResponse>send(new CounterRequest(key, 0, 10, 0, bucket())).toBlocking().single(); assertEquals(0, response1.value()); CounterResponse response2 = cluster().<CounterResponse>send(new CounterRequest(key, 0, 10, 0, bucket())).toBlocking().single(); assertEquals(10, response2.value()); CounterResponse response3 = cluster().<CounterResponse>send(new CounterRequest(key, 0, 10, 0, bucket())).toBlocking().single(); assertEquals(20, response3.value()); assertTrue(response1.cas() != response2.cas()); assertTrue(response2.cas() != response3.cas()); assertMetadataSequence(response1.mutationToken(), response2.mutationToken()); assertMetadataSequence(response2.mutationToken(), response3.mutationToken()); } @Test public void shouldDecrementFromCounter() throws Exception { String key = "counter-decr"; CounterResponse response1 = cluster().<CounterResponse>send(new CounterRequest(key, 100, -10, 0, bucket())).toBlocking().single(); assertEquals(100, response1.value()); CounterResponse response2 = cluster().<CounterResponse>send(new CounterRequest(key, 100, -10, 0, bucket())).toBlocking().single(); assertEquals(90, response2.value()); CounterResponse response3 = cluster().<CounterResponse>send(new CounterRequest(key, 100, -10, 0, bucket())).toBlocking().single(); assertEquals(80, response3.value()); assertTrue(response1.cas() != response2.cas()); assertTrue(response2.cas() != response3.cas()); assertMetadataSequence(response1.mutationToken(), response2.mutationToken()); assertMetadataSequence(response2.mutationToken(), response3.mutationToken()); } @Test public void shouldGetAndTouch() throws Exception { String key = "get-and-touch"; UpsertRequest request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), 3, 0, bucket()); UpsertResponse response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); ReferenceCountUtil.releaseLater(response.content()); Thread.sleep(2000); GetResponse getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket(), false, true, 3)).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("content", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); Thread.sleep(2000); getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket(), false, true, 3)).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("content", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test public void shouldGetAndLock() throws Exception { String key = "get-and-lock"; UpsertRequest request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), bucket()); UpsertResponse response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); ReferenceCountUtil.releaseLater(response.content()); GetResponse getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket(), true, false, 2)).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("content", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), bucket()); response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.EXISTS, response.status()); ReferenceCountUtil.releaseLater(response.content()); GetResponse secondLockResponse = (GetResponse) cluster().send(new GetRequest(key, bucket(), true, false, 2)) .toBlocking().single(); assertEquals(ResponseStatus.TEMPORARY_FAILURE, secondLockResponse.status()); ReferenceCountUtil.releaseLater(secondLockResponse.content()); Thread.sleep(3000); request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), bucket()); response = cluster().<UpsertResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertEquals(ResponseStatus.SUCCESS, response.status()); } /** * Verificiation for MB-15727. * * This test is ignored in versions lower than 4.5 since thats the version where it has been fixed. */ @Test public void shouldGetAndLockWithAppend() throws Exception { assumeMinimumVersionCompatible(4, 5); String key = "get-and-lock-append"; UpsertRequest request = new UpsertRequest(key, Unpooled.copiedBuffer("foo", CharsetUtil.UTF_8), bucket()); UpsertResponse response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); ReferenceCountUtil.releaseLater(response.content()); GetResponse getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket(), true, false, 2)).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("foo", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); AppendResponse appendResponse = cluster().<AppendResponse>send( new AppendRequest(key, getResponse.cas(), Unpooled.copiedBuffer("bar", CharsetUtil.UTF_8), bucket()) ).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertTrue(getResponse.cas() != appendResponse.cas()); ReferenceCountUtil.releaseLater(appendResponse.content()); getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket(), false, false, 0)).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("foobar", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test public void shouldTouch() throws Exception { String key = "touch"; UpsertRequest request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), 3, 0, bucket()); UpsertResponse response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); ReferenceCountUtil.releaseLater(response.content()); Thread.sleep(2000); TouchResponse touchResponse = cluster().<TouchResponse>send(new TouchRequest(key, 3, bucket())).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, touchResponse.status()); ReferenceCountUtil.releaseLater(touchResponse.content()); Thread.sleep(2000); GetResponse getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket())).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("content", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test public void shouldUnlock() throws Exception { String key = "unlock"; UpsertRequest request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), bucket()); UpsertResponse response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); ReferenceCountUtil.releaseLater(response.content()); GetResponse getResponse = cluster().<GetResponse>send(new GetRequest(key, bucket(), true, false, 15)).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, getResponse.status()); assertEquals("content", getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), bucket()); response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.EXISTS, response.status()); ReferenceCountUtil.releaseLater(response.content()); UnlockRequest unlockRequest = new UnlockRequest(key, getResponse.cas(), bucket()); UnlockResponse unlockResponse = cluster().<UnlockResponse>send(unlockRequest).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, unlockResponse.status()); ReferenceCountUtil.releaseLater(unlockResponse.content()); request = new UpsertRequest(key, Unpooled.copiedBuffer("content", CharsetUtil.UTF_8), bucket()); response = cluster().<UpsertResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); ReferenceCountUtil.releaseLater(response.content()); } @Test public void shouldHandleSpecialKeyChars() { String key = "AVERY® READY INDEX®"; String content = "Hello World!"; UpsertRequest upsert = new UpsertRequest(key, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8), bucket()); UpsertResponse response = cluster().<UpsertResponse>send(upsert).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); GetRequest request = new GetRequest(key, bucket()); GetResponse getResponse = cluster().<GetResponse>send(request).toBlocking().single(); assertEquals(content, getResponse.content().toString(CharsetUtil.UTF_8)); ReferenceCountUtil.releaseLater(getResponse.content()); } @Test(expected = IllegalArgumentException.class) public void shouldRejectEmptyKey() { cluster().<GetResponse>send(new GetRequest("", bucket())).toBlocking().single(); } @Test(expected = IllegalArgumentException.class) public void shouldRejectNullKey() { cluster().<GetResponse>send(new GetRequest(null, bucket())).toBlocking().single(); } @Test(expected = IllegalArgumentException.class) public void shouldRejectTooLongKey() { char[] array = new char[251]; Arrays.fill(array, 'a'); cluster().<GetResponse>send(new GetRequest(new String(array), bucket())).toBlocking().single(); } /** * Helper method to assert if the mutation metadata is correct. * * Note that if mutation metadata is disabled, null is expected. * * @param token the token to check * @throws Exception */ private void assertValidMetadata(MutationToken token) throws Exception { if (isMutationMetadataEnabled()) { assertNotNull(token); assertTrue(token.sequenceNumber() > 0); assertTrue(token.vbucketUUID() != 0); assertTrue(token.vbucketID() > 0); assertTrue(token.bucket() != null && token.bucket().equals(bucket())); } else { assertNull(token); } } /** * Helper method to make sure that two consecutive sequences are valid. * * They are valid if the vbucket uuid is the same and the sequence is higher by one. * * @param first the first mutation * @param second the second mutation * @throws Exception */ private void assertMetadataSequence(MutationToken first, MutationToken second) throws Exception { if (isMutationMetadataEnabled()) { assertNotNull(first); assertNotNull(second); assertTrue(first.vbucketUUID() != 0); assertTrue(first.vbucketID() > 0); assertTrue(second.vbucketID() > 0); assertTrue(first.bucket() != null && first.bucket().equals(bucket())); assertEquals(first.bucket(), second.bucket()); assertEquals(first.vbucketUUID(), second.vbucketUUID()); assertTrue((first.sequenceNumber()+1) == second.sequenceNumber()); assertEquals(first.vbucketID(), second.vbucketID()); } else { assertNull(first); assertNull(second); } } }