jetcd Practice II: basic operation

Posted by alconebay on Tue, 07 Dec 2021 15:24:19 +0100

Links to series articles

  1. One of jetcd's actual combat: extreme speed experience
  2. jetcd Practice II: basic operation
  3. jetcd practice 3: advanced operations (transaction, monitoring, lease)

Overview of this article

This article is the second in the jetcd practical combat series. After the previous preparations, we have available etcd cluster environment and gradle parent project, and wrote a helloworld program to connect etcd for a simple experience. Today's practical combat, we focus on those commonly used etcd operations, such as write, read, delete, etc., which can cover most daily scenes, This paper mainly consists of the following parts:

  1. Write the interface class EtcdService.java to define common etcd operations;
  2. Write the implementation EtcdServiceImpl.java of the interface class, which mainly calls the API provided by jetcd to complete the specific etcd operation;
  3. Write the unit test class EtcdServiceImplTest.java, which has many test methods to demonstrate how to use the interface of EtcdService to realize various complex operations;

Source download

  • The complete source code in this actual combat can be downloaded from GitHub. The address and link information are shown in the table below( https://github.com/zq2599/blog_demos):

name

link

remarks

Project Home

https://github.com/zq2599/blog_demos

The project is on the GitHub home page

git warehouse address (https)

https://github.com/zq2599/blog_demos.git

The warehouse address of the source code of the project, https protocol

git warehouse address (ssh)

git@github.com:zq2599/blog_demos.git

The project source code warehouse address, ssh protocol

  • There are multiple folders in the git project. kubebuilder related applications are under the jetcd tutorials folder, as shown in the red box below:
  • There are several subprojects under the jetcd tutorials folder. In this chapter, base operate:

New sub module base operate

  • Add a Gradle sub module named base operate under the parent project jetcd tutorials. The contents of its build.gradle file are as follows:
plugins {
    id 'java-library'
}

// Sub module's own dependency
dependencies {
    api 'io.etcd:jetcd-core'
    api 'org.projectlombok:lombok'
    // The annotation processor will not be passed. Modules that use lombok generated code need to declare the annotation processor themselves
    annotationProcessor 'org.projectlombok:lombok'
    // slf4j's package can only be used by itself. Do not inherit it to other projects, otherwise it is easy to conflict with other log packages
    implementation 'org.slf4j:slf4j-log4j12'
    testImplementation('org.junit.jupiter:junit-jupiter')
}

test {
    useJUnitPlatform()
}
  • Add the interface EtcdService.java, which defines common etcd operations:
package com.bolingcavalry.dao;

import io.etcd.jetcd.Response.Header;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;

/**
 * @Description: Etcd Interface of operation service
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2021/3/30 7:55
 */
public interface EtcdService {

    /**
     * write in
     * @param key
     * @param value
     */
    Header put(String key, String value) throws Exception;

    /**
     * read
     * @param key
     * @return
     */
    String getSingle(String key) throws Exception;


    /**
     * Query operations with additional conditions, such as prefix, result sorting, etc
     * @param key
     * @param getOption
     * @return
     */
    GetResponse getRange(String key, GetOption getOption) throws Exception;

    /**
     * Single delete
     * @param key
     * @return
     */
    long deleteSingle(String key) throws Exception;

    /**
     * Range deletion
     * @param key
     * @param deleteOption
     * @return
     */
    long deleteRange(String key, DeleteOption deleteOption) throws Exception;

    /**
     * Close and release resources
     */
    void close();
}
  • Add the implementation classes corresponding to the above interfaces. It can be seen that most of them directly call the API provided by jetcd:
package com.bolingcavalry.dao.impl;

import com.bolingcavalry.dao.EtcdService;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.Response;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;

import static com.google.common.base.Charsets.UTF_8;

/**
 * @Description: etcd Implementation class of service
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2021/3/30 8:28
 */
public class EtcdServiceImpl implements EtcdService {



    private Client client;

    private String endpoints;

    private Object lock = new Object();

    public EtcdServiceImpl(String endpoints) {
        super();
        this.endpoints = endpoints;
    }

    /**
     * Convert the string to the ByteSequence instance required by the client
     * @param val
     * @return
     */
    public static ByteSequence bytesOf(String val) {
        return ByteSequence.from(val, UTF_8);
    }

    /**
     * Create a new key value client instance
     * @return
     */
    private KV getKVClient(){

        if (null==client) {
            synchronized (lock) {
                if (null==client) {

                    client = Client.builder().endpoints(endpoints.split(",")).build();
                }
            }
        }

        return client.getKVClient();
    }

    @Override
    public void close() {
        client.close();
        client = null;
    }

    @Override
    public Response.Header put(String key, String value) throws Exception {
        return getKVClient().put(bytesOf(key), bytesOf(value)).get().getHeader();
    }

    @Override
    public String getSingle(String key) throws Exception {
        GetResponse getResponse = getKVClient().get(bytesOf(key)).get();

        return getResponse.getCount()>0 ?
               getResponse.getKvs().get(0).getValue().toString(UTF_8) :
               null;
    }

    @Override
    public GetResponse getRange(String key, GetOption getOption) throws Exception {
        return getKVClient().get(bytesOf(key), getOption).get();
    }

    @Override
    public long deleteSingle(String key) throws Exception {
        return getKVClient().delete(bytesOf(key)).get().getDeleted();
    }

    @Override
    public long deleteRange(String key, DeleteOption deleteOption) throws Exception {
        return getKVClient().delete(bytesOf(key), deleteOption).get().getDeleted();
    }
}
  • You must feel very easy to see here. Indeed, calling the above methods can easily complete common reading and writing operations, but many times our operations are not as simple as reading and writing the specified key, such as querying by prefix, returning only quantity without returning data, and deleting in batch until the specified key appears. In fact, as long as we make good use of the interfaces provided by EtcdService, The above complex operations can be easily completed;
  • Next, let's experience the interfaces provided by EtcdService one by one through unit testing, and try to complete various complex operations;

Write unit test cases

  • Add a new unit test class EtcdServiceImplTest, as shown in the following figure. In order to make its internal methods execute in the order specified by us, remember to add annotation @ TestMethodOrder(MethodOrderer.OrderAnnotation.class) to the class:
  • As shown in the red box below, Gradle is used as the test tool by default. Please change it to IntelliJ IDEA in the red box so that the Order, DisplayName and other annotations in the unit test code can take effect:
  • Next, start to write code in EtcdServiceImplTest. First, write a key method, which splices the current time and the input string into a unique string, which can be used as the key (or key prefix) of the following test:
	private static String key(String name) {
        return "/EtcdServiceImplTest/" + name + "-" + System.currentTimeMillis();
    }
  • Define the EtcdServiceImp instance as a static variable, which will be used in subsequent tests. In addition, close the client connection at the end of the test:
	private static EtcdService etcdService = new EtcdServiceImpl();

    @AfterAll
    static void close() {
        etcdService.close();
    }
  • Next, start to experience the basic operation of etcd;

Basic write operation

  • The write operation is very simple, that is, call the put method to pass in key and value. As for verification, before starting the read operation, simply confirm that the header is not empty:
    @Test
    @Order(1)
    @DisplayName("Basic write operation")
    void put() throws Exception {
        Response.Header header = etcdService.put(key("put"), "123");
        assertNotNull(header);
    }

Read operation

  • First test the most basic read operation, and use getSingle method to return a single result:
    @Test
    @Order(2)
    @DisplayName("Basic read operation")
    void getSingle() throws Exception {
        String key = key("getSingle");
        String value = String.valueOf(System.currentTimeMillis());

        // Write first
        etcdService.put(key, value);

        // Reread
        String queryRlt = etcdService.getSingle(key);

        assertEquals(value, queryRlt);
    }
  • Next, with the help of GetOption object, we can perform complex read operations. First, see how to query multiple key value pairs through prefix:
    @Test
    @Order(3)
    @DisplayName("Read operation(Specify prefix)")
    void getWithPrefix() throws Exception {
        String prefix = key("getWithPrefix");

        // Write ten first
        int num = 10;

        for (int i=0;i<num;i++) {
            // Write, each key is different
            etcdService.put(prefix + i, String.valueOf(i));
        }

        // Query with prefix. Note that the input parameter key and prefix are the same value
        GetOption getOption = GetOption.newBuilder().withPrefix(EtcdServiceImpl.bytesOf(prefix)).build();
        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // The total should be ten
        assertEquals(num, getResponse.getCount());
    }
  • Assuming that there are ten results in total, you can also control to return only five records (but the total field is still ten):
    @Test
    @Order(4)
    @DisplayName("Read operation(appoint KeyValue Number of results)")
    void getWithLimit() throws Exception {
        String prefix = key("getWithLimit");

        // Write ten first
        int num = 10;
        int limit = num/2;

        for (int i=0;i<num;i++) {
            // Write, each key is different
            etcdService.put(prefix + i, String.valueOf(i));
        }

        // The number of prefixed queries should be ten, and the number is limited to five
        GetOption getOption = GetOption.newBuilder()
                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                .withLimit(limit)
                .build();

        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // The total is still ten
        assertEquals(num, getResponse.getCount());
        // The number of results is related to the limit, which is 5
        assertEquals(limit, getResponse.getKvs().size());
    }
  • The revision field is the global version number of etcd. Each write will correspond to a revision value. You can use the revision value as a query condition to find the value of a previous version of the specified key:
    @Test
    @Order(5)
    @DisplayName("Read operation(appoint revision)")
    void getWithRevision() throws Exception {
        String key = key("getWithRevision");

        // Write ten first
        int num = 10;
        int limit = num/2;

        // revision on first write
        long firstRevision = 0L;

        // value written for the first time
        String firstValue = null;

        // Last written value
        String lastValue = null;

        for (int i=0;i<num;i++) {
            // Write the same key ten times, and the value is different each time
            String value = String.valueOf(i);
            // Note that the key has not changed
            Response.Header header = etcdService.put(key, value);

            // Save the revision and value written for the first time, and then use revision to get the value. The comparison with value should be equal
            if (0==i) {
                firstRevision = header.getRevision();
                firstValue = value;
            } else if ((num-1)==i) {
                // Record the last written value
                lastValue = value;
            }
        }


        // The recorded value written for the first time and the value written for the last time should be different
        assertNotEquals(firstValue, lastValue);

        // If you only use key to search without other conditions, the found value should be equal to the last written value
        assertEquals(lastValue, etcdService.getSingle(key));

        // Specify the revision written for the first time in the query criteria
        GetOption getOption = GetOption.newBuilder()
                              .withRevision(firstRevision)
                              .build();

        GetResponse getResponse = etcdService.getRange(key, getOption);

        // The total is one
        assertEquals(1, getResponse.getCount());

        // The value of the result should be equal to the value written for the first time in the previous record
        assertEquals(firstValue, getResponse.getKvs().get(0).getValue().toString(UTF_8));
    }
  • When there are multiple query results, you can also sort the results. Both key and value can be used as sorting fields, and you can choose ascending or descending order:
    @Test
    @Order(6)
    @DisplayName("Read operation(Result sorting)")
    void getWithOrder() throws Exception {
        String prefix = key("getWithOrder");

        // Write ten entries first. The key and value of each entry are different
        int num = 10;

        // First written key
        String firstKey = null;
        // value written for the first time
        String firstValue = null;
        // Last written key
        String lastKey = null;
        // Last written value
        String lastValue = null;

        for (int i=0;i<num;i++) {
            String key = prefix + i;
            String value = String.valueOf(i);
            // Write, each key is different
            etcdService.put(key, value);

            // Save the key and value written for the first time and the key and value written for the last time to the corresponding variables
            if(0==i) {
                firstKey = key;
                firstValue = value;
            } else if((num-1)==i) {
                lastKey = key;
                lastValue = value;
            }
        }


        // For the first query, the results are sorted by key, from large to small
        GetOption getOption = GetOption.newBuilder()
                                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                                .withSortField(GetOption.SortTarget.KEY)
                                .withSortOrder(GetOption.SortOrder.DESCEND)
                                .build();

        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // The total is still ten
        assertEquals(num, getResponse.getCount());

        // Get the first query result
        KeyValue firstResult = getResponse.getKvs().get(0);

        // Because the query results are from large to small, the first item of the query results should be written last (key is lastKey and value is lastValue)
        assertEquals(lastKey, firstResult.getKey().toString(UTF_8));
        assertEquals(lastValue, firstResult.getValue().toString(UTF_8));


        // For the second query, the results are sorted by key, from small to large
        getOption = GetOption.newBuilder()
                    .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                    .withSortField(GetOption.SortTarget.KEY)
                    .withSortOrder(GetOption.SortOrder.ASCEND)
                    .build();

        getResponse = etcdService.getRange(prefix, getOption);

        // The total is still ten
        assertEquals(num, getResponse.getCount());

        // Get the first query result
        firstResult = getResponse.getKvs().get(0);

        // Because it is from small to large, the first entry of the query result should be written for the first time (key is firstKey and value is firstValue)
        assertEquals(firstKey, firstResult.getKey().toString(UTF_8));
        assertEquals(firstValue, firstResult.getValue().toString(UTF_8));
    }
  • Specify that there is only key but no value in the returned result:
    @Test
    @Order(7)
    @DisplayName("Read operation(Return only key)")
    void getOnlyKey() throws Exception {
        String key = key("getOnlyKey");
        // Write a record
        etcdService.put(key, String.valueOf(System.currentTimeMillis()));

        // Only key is specified in the query criteria
        GetOption getOption = GetOption.newBuilder()
                            .withKeysOnly(true)
                            .build();

        GetResponse getResponse = etcdService.getRange(key, getOption);

        assertEquals(1, getResponse.getCount());

        KeyValue keyValue = getResponse.getKvs().get(0);

        assertNotNull(keyValue);

        assertEquals(key, keyValue.getKey().toString(UTF_8));

        // value should be empty
        assertTrue(keyValue.getValue().isEmpty());
    }
  • The returned result contains only quantity, excluding key and value:
    @Test
    @Order(8)
    @DisplayName("Read operation(Return quantity only)")
    void getOnlyCount() throws Exception {
        String key = key("getOnlyCount");
        // Write a record
        etcdService.put(key, String.valueOf(System.currentTimeMillis()));

        // Only key is specified in the query criteria
        GetOption getOption = GetOption.newBuilder()
                            .withCountOnly(true)
                            .build();

        GetResponse getResponse = etcdService.getRange(key, getOption);

        // The quantity should be 1
        assertEquals(1, getResponse.getCount());

        // KeyValue should be empty
        assertTrue(getResponse.getKvs().isEmpty());
    }
  • Assuming that etcd has three keys: a1, a2 and a3, the three keys can be found through prefix a. at the same time, an endKey query condition can be added. Assuming that endKey is equal to a2, the search will stop and return when a2 is found, and only a1, excluding a2, will be returned in the return value. In other words, the value before endKey will be returned:
    @Test
    @Order(9)
    @DisplayName("Read operation(The specified key It's over)")
    void getWithEndKey() throws Exception {
        String prefix = key("getWithEndKey");
        String endKey = null;

        int num = 10;

        for (int i=0;i<num;i++) {
            String key = prefix + i;
            // Write, each key is different
            etcdService.put(key, String.valueOf(i));

            // A total of ten records are written, and the key of Article 9 is saved as endKey
            if ((num-2)==i) {
                endKey = key;
            }
        }

        // The endKey specified in the query criteria is the key of the ninth record written above
        // Note that the query result does not contain the endKey record, that is, only the first eight records are returned
        GetOption getOption = GetOption.newBuilder()
                .withRange(EtcdServiceImpl.bytesOf(endKey))
                .build();

        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // Note that the query result does not contain the endKey record, that is, only the first eight records are returned
        assertEquals(num-2, getResponse.getCount());
    }
  • The above is the common usage of read operation. Next, see delete;

Delete operation

  • The most basic deletion is to call the deleteSingle method:
    @Test
    @Order(10)
    @DisplayName("Single delete")
    void deleteSingle() throws Exception {
        String key = key("deleteSingle");

        // Write a record
        etcdService.put(key, String.valueOf(System.currentTimeMillis()));

        // You should be able to find it at this time
        assertNotNull(etcdService.getSingle(key));

        // delete
        etcdService.deleteSingle(key);

        // It should not be found at this time
        assertNull(etcdService.getSingle(key));
    }
  • With the DeleteOption object, more types of deletion can be realized. The following is to delete all records with the specified prefix:
   @Test
    @Order(11)
    @DisplayName("delete(Specify prefix)")
    void deleteWithPrefix() throws Exception {
        String prefix = key("deleteWithPrefix");

        int num = 10;

        // When writing, each key is different, but has the same prefix
        for (int i=0;i<num;i++) {
            etcdService.put(prefix + i, String.valueOf(i));
        }

        GetOption getOption = GetOption.newBuilder()
                                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                                .build();

        // The total should be ten at this time
        assertEquals(num, etcdService.getRange(prefix, getOption).getCount());

        // The deletion condition is to specify a prefix
        DeleteOption deleteOption = DeleteOption.newBuilder()
                                    .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                                    .build();

        // delete
        etcdService.deleteRange(prefix, deleteOption);

        // Check again after deletion. The total should be 0
        assertEquals(0, etcdService.getRange(prefix, getOption).getCount());
    }
  • Similar to the endKey of the read operation, the delete operation also has an endKey parameter. Assuming that etcd has three keys: a1, a2 and a3, these three keys can be deleted through prefix a. at the same time, an endKey deletion condition can be added. Assuming that endKey is equal to a2, the deletion will stop and return when a2 is found. Only a1 records are deleted, excluding a2, In other words, records before endKey will be deleted:
    @Test
    @Order(11)
    @DisplayName("delete(Delete to specified key It's over)")
    void deleteWithEndKey() throws Exception {
        String prefix = key("deleteWithEndKey");

        int num = 10;
        String endKey = null;

        // When writing, each key is different, but has the same prefix
        for (int i=0;i<num;i++) {
            String key = prefix + i;

            etcdService.put(key, String.valueOf(i));

            // Save the key of the ninth record in the endKey variable
            if((num-2)==i) {
                endKey = key;
            }
        }

        GetOption getOption = GetOption.newBuilder()
                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                .build();

        // The total should be ten at this time
        assertEquals(num, etcdService.getRange(prefix, getOption).getCount());

        // The deletion condition is to specify the prefix and stop the deletion operation when the key of the ninth record is encountered. At this time, neither the ninth nor the tenth record will be deleted
        DeleteOption deleteOption = DeleteOption.newBuilder()
                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                .withRange(EtcdServiceImpl.bytesOf(endKey))
                .build();

        // delete
        etcdService.deleteRange(prefix, deleteOption);

        // Check again after deletion. The total should be two

        assertEquals(2, etcdService.getRange(prefix, getOption).getCount());
    }
  • At this point, the coding is completed and the unit test is performed;

Perform unit tests

  • Click the button in the red box below, and click Run EtcdServiceImplTest in the pop-up menu to start the unit test:
  • As shown in the following figure, the unit test passed:
  • So far, the actual operation of etcd using jetcd has been completed. I hope it can bring some references to your development. In the next chapter, let's operate some etcd features, including transaction, monitoring and lease;