Disruptor note 3: basic operation of ring queue (without disruptor class)

Welcome to my GitHub

Here we classify and summarize all the original works of Xinchen (including supporting source code): https://github.com/zq2599/blog_demos

Links to the disruptor Notes Series

  1. quick get start
  2. Disruptor class analysis
  3. Basic operation of ring queue (without Disruptor class)
  4. Summary of event consumption knowledge points
  5. Event consumption practice
  6. Common scenarios
  7. Waiting strategy
  8. Supplement of knowledge points (final part)

Overview of this article

  • This article is the third in the disruptor notes series. Its main task is to encode and realize message production and consumption, and One of the disruptor notes: getting started The difference is that this development does not use the destroyer class, and the operations related to the ring buffer (ring queue) are implemented by writing their own code;
  • This method of operating Ring Buffer without the Disruptor class is not suitable for production environment, but it is an efficient learning method in the process of learning Disruptor. After this practical battle, you can be more handy in various scenarios such as development, debugging and optimization when using Disruptor in the future;
  • Simple news production and consumption can no longer meet our learning enthusiasm. Today's actual combat will challenge the following three scenarios:
  • 100 events, single consumer consumption;
  • 100 events, three consumers, each consuming the 100 events alone;
  • 100 events, three consumers consume the 100 events together;

Previous review

  • In order to complete the actual combat of this chapter, the above Disruptor notes II: analysis of disruptor classes We have done enough research and analysis, and we recommend watching. Here we briefly review the following core functions of the Disruptor class, which we want to implement when coding:
  1. Create ring queue (RingBuffer object)
  2. Create a SequenceBarrier object to receive consumable events in ringBuffer
  3. Create a BatchEventProcessor responsible for consuming events
  4. Bind exception handling class of BatchEventProcessor object
  5. Call ringBuffer.addGatingSequences to pass the consumer's Sequence to ringBuffer
  6. Start an independent thread to execute the business logic of consumption events
  • The theoretical analysis has been completed, and then start coding;

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

The project is on the GitHub home page

git warehouse address (https)

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 this git project. The source code of this actual battle is in the disruptor tutorials folder, as shown in the red box below:
  • Disruptor tutorials is a parent project with multiple modules. The actual module in this article is low level operate, as shown in the red box below:

development

  • Entering the coding stage, today's task is to challenge the following three scenarios:
  1. 100 events, single consumer consumption;
  2. 100 events, three consumers, each consuming the 100 events alone;
  3. 100 events, three consumers consume the 100 events together;
  • Let's build the project first, then write public code, such as event definition and event factory, and finally develop each scenario;
  • Add a module named low-level-operate in the parent project disruptor tutorials, and its build.gradle is as follows:
plugins {
    id 'org.springframework.boot'
}

dependencies {
    implementation 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.lmax:disruptor'

    testImplementation('org.springframework.boot:spring-boot-starter-test')
}
  • Then the springboot startup class:
package com.bolingcavalry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LowLevelOperateApplication {
	public static void main(String[] args) {
		SpringApplication.run(LowLevelOperateApplication.class, args);
	}
}
  • Event class, which is the definition of event:
package com.bolingcavalry.service;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@ToString
@NoArgsConstructor
public class StringEvent {
    private String value;
}
  • Event factory, which defines how to create event objects in memory:
package com.bolingcavalry.service;

import com.lmax.disruptor.EventFactory;

public class StringEventFactory implements EventFactory<StringEvent> {
    @Override
    public StringEvent newInstance() {
        return new StringEvent();
    }
}
  • The event production class defines how to convert business logic events into disruptor events and publish them to the ring queue for consumption:
package com.bolingcavalry.service;

import com.lmax.disruptor.RingBuffer;

public class StringEventProducer {

    // Ring queue for storing data
    private final RingBuffer<StringEvent> ringBuffer;

    public StringEventProducer(RingBuffer<StringEvent> ringBuffer) {
        this.ringBuffer = ringBuffer;
    }

    public void onData(String content) {

        // ringBuffer is a queue, and its next method returns the location after the next record, which is an available location
        long sequence = ringBuffer.next();

        try {
            // The event fetched from the sequence position is an empty event
            StringEvent stringEvent = ringBuffer.get(sequence);
            // Add business information to an empty event
            stringEvent.setValue(content);
        } finally {
            // release
            ringBuffer.publish(sequence);
        }
    }
}
  • Event processing class, the specific business processing logic after receiving the event:
package com.bolingcavalry.service;

import com.lmax.disruptor.EventHandler;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Consumer;

@Slf4j
public class StringEventHandler implements EventHandler<StringEvent> {

    public StringEventHandler(Consumer<?> consumer) {
        this.consumer = consumer;
    }

    // Externally, the consumer implementation class can be passed in. Each time a message is processed, the consumer's accept method will be executed once
    private Consumer<?> consumer;

    @Override
    public void onEvent(StringEvent event, long sequence, boolean endOfBatch) throws Exception {
        log.info("sequence [{}], endOfBatch [{}], event : {}", sequence, endOfBatch, event);

        // Here, the delay is 100ms, which is the time-consuming time of simulating the logic of consumption events
        Thread.sleep(100);

        // If the consumer is passed in externally, the accept method must be executed once
        if (null!=consumer) {
            consumer.accept(null);
        }
    }
}
  • An interface is defined. Messages are produced externally by calling the methods of the interface, and several constants are put in it, which will be used later:
package com.bolingcavalry.service;

public interface LowLevelOperateService {
    /**
     * Number of consumers
     */
    int CONSUMER_NUM = 3;

    /**
     * Ring buffer size
     */
    int BUFFER_SIZE = 16;

    /**
     * Publish an event
     * @param value
     * @return
     */
    void publish(String value);

    /**
     * Returns the total number of tasks that have been processed
     * @return
     */
    long eventCount();
}
  • The above is the public code. Next, implement the three previously planned scenarios one by one;

100 events, single consumer consumption

  • This is the simplest function to realize the functions of publishing news and consuming by a single consumer. The code is as follows. Several points to note will be mentioned later:
package com.bolingcavalry.service.impl;

import com.bolingcavalry.service.*;
import com.lmax.disruptor.BatchEventProcessor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.SequenceBarrier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

@Service("oneConsumer")
@Slf4j
public class OneConsumerServiceImpl implements LowLevelOperateService {

    private RingBuffer<StringEvent> ringBuffer;

    private StringEventProducer producer;

    /**
     * Total number of statistics messages
     */
    private final AtomicLong eventCount = new AtomicLong();

    private ExecutorService executors;

    @PostConstruct
    private void init() {
        // Prepare an anonymous class and pass it to the event handling class of the disruptor,
        // In this way, each time an event is processed, the total number of events that have been processed will be printed
        Consumer<?> eventCountPrinter = new Consumer<Object>() {
            @Override
            public void accept(Object o) {
                long count = eventCount.incrementAndGet();
                log.info("receive [{}] event", count);
            }
        };

        // Create ring queue instance
        ringBuffer = RingBuffer.createSingleProducer(new StringEventFactory(), BUFFER_SIZE);

        // Prepare thread pool
        executors = Executors.newFixedThreadPool(1);

        //Create SequenceBarrier
        SequenceBarrier sequenceBarrier = ringBuffer.newBarrier();

        // Create a working class for event handling, which executes StringEventHandler to handle events
        BatchEventProcessor<StringEvent> batchEventProcessor = new BatchEventProcessor<>(
                ringBuffer,
                sequenceBarrier,
                new StringEventHandler(eventCountPrinter));

        // Pass the consumer's sequence to the ring queue
        ringBuffer.addGatingSequences(batchEventProcessor.getSequence());

        // Take events and consume them in a separate thread
        executors.submit(batchEventProcessor);

        // producer
        producer = new StringEventProducer(ringBuffer);
    }

    @Override
    public void publish(String value) {
        producer.onData(value);
    }

    @Override
    public long eventCount() {
        return eventCount.get();
    }
}
  • The above code has the following points to pay attention to:
  1. Create ring queue RingBuffer instance yourself
  2. Prepare your own thread pool. The threads in it are used to obtain and consume messages
  3. Create the BatchEventProcessor instance yourself and pass the event processing class in
  4. Create a sequenceBarrier through ringBuffer and pass it to the BatchEventProcessor instance for use
  5. Pass the sequence of BatchEventProcessor to ringBuffer to ensure that the production and consumption of ringBuffer will not be confused
  6. Starting the thread pool means that the BatchEventProcessor instance continuously obtains and consumes events from the ringBuffer in an independent thread;
  • In order to verify whether the above code can work normally, I write a unit test class here, as shown below. The logic is very simple. Call OneConsumerServiceImpl.publish method 100 times to generate 100 events, and then check whether the total number of consumption events recorded by OneConsumerServiceImpl is equal to 100:
package com.bolingcavalry.service.impl;

import com.bolingcavalry.service.LowLevelOperateService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class LowLeverOperateServiceImplTest {

    @Autowired
    @Qualifier("oneConsumer")
    LowLevelOperateService oneConsumer;

    private static final int EVENT_COUNT = 100;

    private void testLowLevelOperateService(LowLevelOperateService service, int eventCount, int expectEventCount) throws InterruptedException {
        for(int i=0;i<eventCount;i++) {
            log.info("publich {}", i);
            service.publish(String.valueOf(i));
        }

        // Asynchronous consumption, so delayed waiting is required
        Thread.sleep(10000);

        // The total number of events consumed should be equal to the number of events published
        assertEquals(expectEventCount, service.eventCount());
    }

    @Test
    public void testOneConsumer() throws InterruptedException {
        log.info("start testOneConsumerService");
        testLowLevelOperateService(oneConsumer, EVENT_COUNT, EVENT_COUNT);
    }
  • Note that if you directly click the icon on the IDEA to execute the unit test, remember to check the option in the red box below, otherwise compilation failure may occur:
  • Execute the above unit test class. The results are shown in the figure below. The production and consumption of messages meet expectations, and the consumption logic is executed in an independent thread:
  • Continue to challenge the next scene;

100 events, three consumers, each consuming the 100 events alone

  • This scenario also exists in kafka, that is, the group s of the three consumers are different, so the two consumers consume each message once;
  • Therefore, for 100 events, each of the three consumers will consume the 100 events independently, a total of 300 times;
  • The code is as follows. There are several points to be noted later:
package com.bolingcavalry.service.impl;

import com.bolingcavalry.service.*;
import com.lmax.disruptor.BatchEventProcessor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.SequenceBarrier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

@Service("multiConsumer")
@Slf4j
public class MultiConsumerServiceImpl implements LowLevelOperateService {

    private RingBuffer<StringEvent> ringBuffer;

    private StringEventProducer producer;

    /**
     * Total number of statistics messages
     */
    private final AtomicLong eventCount = new AtomicLong();

    /**
     * Produce a BatchEventProcessor instance and start a separate thread to get and consume messages
     * @param executorService
     */
    private void addProcessor(ExecutorService executorService) {
        // Prepare an anonymous class and pass it to the event handling class of the disruptor,
        // In this way, each time an event is processed, the total number of events that have been processed will be printed
        Consumer<?> eventCountPrinter = new Consumer<Object>() {
            @Override
            public void accept(Object o) {
                long count = eventCount.incrementAndGet();
                log.info("receive [{}] event", count);
            }
        };

        BatchEventProcessor<StringEvent> batchEventProcessor = new BatchEventProcessor<>(
                ringBuffer,
                ringBuffer.newBarrier(),
                new StringEventHandler(eventCountPrinter));

        // Pass the sequence instance of the current consumer to ringBuffer
        ringBuffer.addGatingSequences(batchEventProcessor.getSequence());

        // Start a stand-alone thread to get and consume events
        executorService.submit(batchEventProcessor);
    }

    @PostConstruct
    private void init() {
        ringBuffer = RingBuffer.createSingleProducer(new StringEventFactory(), BUFFER_SIZE);

        ExecutorService executorService = Executors.newFixedThreadPool(CONSUMER_NUM);

        // Create multiple consumers and get and consume events in separate threads
        for (int i=0;i<CONSUMER_NUM;i++) {
            addProcessor(executorService);
        }

        // producer
        producer = new StringEventProducer(ringBuffer);
    }

    @Override
    public void publish(String value) {
        producer.onData(value);
    }

    @Override
    public long eventCount() {
        return eventCount.get();
    }
}
  • The above code is not different from the previous OneConsumerServiceImpl. It mainly creates multiple BatchEventProcessor instances and submits them in the process pool respectively;
  • The verification method is still unit test. Just add code in LowLeverOperateServiceImplTest.java. Note that the third parameter of testLowLevelOperateService is EVENT_COUNT * LowLevelOperateService.CONSUMER_NUM, indicating that the expected number of consumed messages is * * 300 * *:
 	@Autowired
    @Qualifier("multiConsumer")
    LowLevelOperateService multiConsumer;

    @Test
    public void testMultiConsumer() throws InterruptedException {
        log.info("start testMultiConsumer");
        testLowLevelOperateService(multiConsumer, EVENT_COUNT, EVENT_COUNT * LowLevelOperateService.CONSUMER_NUM);
    }
  • Execute the unit test, as shown in the figure below. A total of 300 events are consumed, and three consumers are in the fixed thread:

100 events, three consumers consume the 100 events together

  • The last practical battle of this article is to release 100 events, and then let three consumers consume 100 together (for example, 33 for A, 33 for B, and 34 for C);
  • The BatchEventProcessor used earlier is used for independent consumption and is not suitable for multiple consumers to consume together. This scenario of multiple consumption and common consumption needs to be completed with the help of WorkerPool. The name is still very vivid: there are many workers in a pool. Put their tasks into the pool. Each worker handles part of them and everyone works together to complete the tasks;
  • The consumer passing in the WorkerPool needs to implement the WorkHandler interface, so an implementation class is added:
package com.bolingcavalry.service;

import com.lmax.disruptor.WorkHandler;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Consumer;

@Slf4j
public class StringWorkHandler implements WorkHandler<StringEvent> {

    public StringWorkHandler(Consumer<?> consumer) {
        this.consumer = consumer;
    }

    // Externally, the consumer implementation class can be passed in. Each time a message is processed, the consumer's accept method will be executed once
    private Consumer<?> consumer;

    @Override
    public void onEvent(StringEvent event) throws Exception {
        log.info("work handler event : {}", event);

        // Here, the delay is 100ms, which is the time-consuming time of simulating the logic of consumption events
        Thread.sleep(100);

        // If the consumer is passed in externally, the accept method must be executed once
        if (null!=consumer) {
            consumer.accept(null);
        }
    }
}
  • Add a new service class to realize the logic of common consumption. Several points to note will be mentioned later:
package com.bolingcavalry.service.impl;

import com.bolingcavalry.service.*;
import com.lmax.disruptor.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

@Service("workerPoolConsumer")
@Slf4j
public class WorkerPoolConsumerServiceImpl implements LowLevelOperateService {

    private RingBuffer<StringEvent> ringBuffer;

    private StringEventProducer producer;

    /**
     * Total number of statistics messages
     */
    private final AtomicLong eventCount = new AtomicLong();

    @PostConstruct
    private void init() {
        ringBuffer = RingBuffer.createSingleProducer(new StringEventFactory(), BUFFER_SIZE);

        ExecutorService executorService = Executors.newFixedThreadPool(CONSUMER_NUM);

        StringWorkHandler[] handlers = new StringWorkHandler[CONSUMER_NUM];

        // Create multiple StringWorkHandler instances and put them into an array
        for (int i=0;i < CONSUMER_NUM;i++) {
            handlers[i] = new StringWorkHandler(o -> {
                long count = eventCount.incrementAndGet();
                log.info("receive [{}] event", count);
            });
        }

        // Create a WorkerPool instance and pass in the array of StringWorkHandler instances to represent the number of common consumers
        WorkerPool<StringEvent> workerPool = new WorkerPool<>(ringBuffer, ringBuffer.newBarrier(), new IgnoreExceptionHandler(), handlers);

        // This sentence is very important. If it is removed, there will be the problem of repeated consumption of the same event
        ringBuffer.addGatingSequences(workerPool.getWorkerSequences());

        workerPool.start(executorService);

        // producer
        producer = new StringEventProducer(ringBuffer);
    }

    @Override
    public void publish(String value) {
        producer.onData(value);
    }

    @Override
    public long eventCount() {
        return eventCount.get();
    }
}
  • In the above code, you should pay attention to the following two points:
  1. After the StringWorkHandler array is passed into the WorkerPool, each StringWorkHandler instance is put into a new WorkProcessor instance. The WorkProcessor implements the Runnable interface and will submit the WorkProcessor to the thread pool when executing workerPool.start;
  2. Compared with the previous independent consumption, the biggest feature of common consumption is that the ringBuffer.addGatingSequences method is called only once, that is, three consumers share a sequence instance;
  3. The verification method is still unit test. Just add code to LowLeverOperateServiceImplTest.java. Note that the third parameter of testWorkerPoolConsumer is EVENT_COUNT, indicating that the expected number of consumed messages is * * 100 * *:
 	@Autowired
    @Qualifier("workerPoolConsumer")
    LowLevelOperateService workerPoolConsumer;
    
    @Test
    public void testWorkerPoolConsumer() throws InterruptedException {
        log.info("start testWorkerPoolConsumer");
        testLowLevelOperateService(workerPoolConsumer, EVENT_COUNT, EVENT_COUNT);
    }
  • Execute the unit test as shown in the figure below. Three consumers consume 100 events in total, and the three consumers are in different threads:
  • So far, we have completed the message production and consumption of three common scenarios without using the destroyer class. I believe you also have a deep understanding of the underlying implementation of the destroyer. In the future, you will be more comfortable in using or optimizing the destroyer;

Posted by falcon1 on Wed, 17 Nov 2021 17:00:36 -0800