From 1fb340f1125e8a2415743dcf2af351e63cbd0c90 Mon Sep 17 00:00:00 2001 From: nothub <48992448+nothub@users.noreply.github.com> Date: Sun, 7 Feb 2021 01:36:34 +0100 Subject: [PATCH] =?UTF-8?q?0.1=20=F0=9F=A6=96=20reg=20unreg=20for=20object?= =?UTF-8?q?s=20with=20sub=20fields=20sync=20threads=20by=20event=20type=20?= =?UTF-8?q?async=20test=20qol=20consumer=20more=20tests=20fix=20sub=20comp?= =?UTF-8?q?are=20also=20remove=20proxy=20method=20on(Event)=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 17 +++++ .github/dependabot.yml | 15 ++++ .github/workflows/tests.yml | 34 +++++++++ .gitignore | 10 +++ README.md | 44 ++++++++++++ pom.xml | 55 ++++++++++++++ .../java/cc/neckbeard/tinypubsub/Bus.java | 71 +++++++++++++++++++ .../java/cc/neckbeard/tinypubsub/Config.java | 12 ++++ .../java/cc/neckbeard/tinypubsub/Event.java | 15 ++++ .../java/cc/neckbeard/tinypubsub/Sub.java | 28 ++++++++ .../tinypubsub/tests/AsyncTests.java | 50 +++++++++++++ .../tinypubsub/tests/BooleanEvent.java | 13 ++++ .../neckbeard/tinypubsub/tests/BusTests.java | 39 ++++++++++ .../neckbeard/tinypubsub/tests/RegTest.java | 61 ++++++++++++++++ .../tinypubsub/tests/SortingTests.java | 37 ++++++++++ 15 files changed, 501 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/cc/neckbeard/tinypubsub/Bus.java create mode 100644 src/main/java/cc/neckbeard/tinypubsub/Config.java create mode 100644 src/main/java/cc/neckbeard/tinypubsub/Event.java create mode 100644 src/main/java/cc/neckbeard/tinypubsub/Sub.java create mode 100644 src/test/java/cc/neckbeard/tinypubsub/tests/AsyncTests.java create mode 100644 src/test/java/cc/neckbeard/tinypubsub/tests/BooleanEvent.java create mode 100644 src/test/java/cc/neckbeard/tinypubsub/tests/BusTests.java create mode 100644 src/test/java/cc/neckbeard/tinypubsub/tests/RegTest.java create mode 100644 src/test/java/cc/neckbeard/tinypubsub/tests/SortingTests.java diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..922a7c2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{md, markdown}] +trim_trailing_whitespace = false + +[*.{yml, yaml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..00e5b7b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + time: "12:00" + timezone: "Europe/Berlin" + assignees: + - "nothub" + reviewers: + - "nothub" + commit-message: + prefix: "maven" + include: "scope" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5c46dad --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,34 @@ +name: 🦖 + +on: + push: + branches: + - 'master' + tags: + - '*' + pull_request: + branches: + - '*' + +jobs: + tests: + runs-on: ubuntu-latest + steps: + + - name: checkout + uses: actions/checkout@v2 + + - name: java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: cache + uses: actions/cache@v2 + with: + path: ~/.m2 + key: '${{ runner.os }}-m2-${{ hashFiles(''**/pom.xml'') }}' + restore-keys: '${{ runner.os }}-m2' + + - name: test + run: mvn --batch-mode --show-version --errors --file pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36a1562 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +* +!*/ + +!/*.md +!/.editorconfig +!/.github/** +!/.gitignore +!/LICENSE +!/pom.xml +!/src/** diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7de8e0 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +###### register event consumer + +```java +class Example { + void example() { + Consumer consumer = System.out::println; + Sub sub = new Sub<>(BooleanEvent.class, consumer); + Bus bus = new Bus(); + bus.reg(sub); + bus.pub(new BooleanEvent(true)); + } +} +``` + +--- + +###### register consumer fields + +```java +class Example { + boolean invoked = false; + Sub sub = new Sub<>(BooleanEvent.class, e -> invoked = e.bool); + + void example() { + Bus bus = new Bus(); + bus.regFields(this); + bus.pub(new BooleanEvent(true)); + } +} +``` + +--- + +###### listen to canceled events + +```java +public class EventCanceledEvent extends Event { + Event e; + + public EventCanceledEvent(Event e) { + this.e = e; + } +} +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fc08543 --- /dev/null +++ b/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + cc.neckbeard + TinyPubSub + 0.1 + + ${project.artifactId} + jar + + + 1.8 + ${java.version} + ${java.version} + UTF-8 + + + + clean test + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + + + + org.jetbrains + annotations + 20.1.0 + + + org.junit.jupiter + junit-jupiter-engine + 5.7.1 + test + + + + diff --git a/src/main/java/cc/neckbeard/tinypubsub/Bus.java b/src/main/java/cc/neckbeard/tinypubsub/Bus.java new file mode 100644 index 0000000..df487ff --- /dev/null +++ b/src/main/java/cc/neckbeard/tinypubsub/Bus.java @@ -0,0 +1,71 @@ +package cc.neckbeard.tinypubsub; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.stream.Collectors; + +public class Bus { + + private final Map, Object> locks = new ConcurrentHashMap<>(); + private final Map, ConcurrentSkipListSet> subs = new ConcurrentHashMap<>(); + + private static Set getSubs(Object obj) { + return Arrays + .stream(obj.getClass().getDeclaredFields()) + .filter(field -> field.getType().equals(Sub.class)) + .map(field -> { + boolean access = field.isAccessible(); + field.setAccessible(true); + try { + return field.get(obj); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } finally { + field.setAccessible(access); + } + return null; + }) + .filter(Objects::nonNull) + .map(o -> (Sub) o) + .collect(Collectors.toSet()); + } + + public void reg(@NotNull Sub sub) { + synchronized (locks.computeIfAbsent(sub.type, c -> new Object())) { + if (this.subs.computeIfAbsent(sub.type, c -> new ConcurrentSkipListSet<>()).contains(sub)) return; + this.subs.get(sub.type).add(sub); + } + } + + public void regFields(@NotNull Object o) { + getSubs(o).forEach(this::reg); + } + + public void unreg(@NotNull Sub sub) { + synchronized (locks.computeIfAbsent(sub.type, c -> new Object())) { + if (this.subs.get(sub.type) == null) return; + this.subs.get(sub.type).removeIf(s -> s.equals(sub)); + } + } + + public void unregFields(@NotNull Object o) { + getSubs(o).forEach(this::unreg); + } + + public void pub(@NotNull Event e) { + synchronized (locks.computeIfAbsent(e.getClass(), c -> new Object())) { + this.subs.computeIfAbsent(e.getClass(), c -> new ConcurrentSkipListSet<>()) + .forEach(sub -> { + if (e.isCancelled()) return; + sub.consumer.accept(e); + }); + } + } + +} diff --git a/src/main/java/cc/neckbeard/tinypubsub/Config.java b/src/main/java/cc/neckbeard/tinypubsub/Config.java new file mode 100644 index 0000000..d50121b --- /dev/null +++ b/src/main/java/cc/neckbeard/tinypubsub/Config.java @@ -0,0 +1,12 @@ +package cc.neckbeard.tinypubsub; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +// TODO: @DarkiBoi + +@Retention(RetentionPolicy.RUNTIME) +public @interface Config { + int prio() default 0; + Class type(); +} diff --git a/src/main/java/cc/neckbeard/tinypubsub/Event.java b/src/main/java/cc/neckbeard/tinypubsub/Event.java new file mode 100644 index 0000000..c92f4e3 --- /dev/null +++ b/src/main/java/cc/neckbeard/tinypubsub/Event.java @@ -0,0 +1,15 @@ +package cc.neckbeard.tinypubsub; + +public abstract class Event { + + private boolean cancelled; + + public void cancel() { + this.cancelled = true; + } + + public boolean isCancelled() { + return this.cancelled; + } + +} diff --git a/src/main/java/cc/neckbeard/tinypubsub/Sub.java b/src/main/java/cc/neckbeard/tinypubsub/Sub.java new file mode 100644 index 0000000..c0a54e7 --- /dev/null +++ b/src/main/java/cc/neckbeard/tinypubsub/Sub.java @@ -0,0 +1,28 @@ +package cc.neckbeard.tinypubsub; + +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +public class Sub implements Comparable> { + + public final int prio; + public final Class type; + public final Consumer consumer; + + public Sub(int prio, Class type, Consumer consumer) { + this.prio = prio; + this.type = type; + this.consumer = consumer; + } + + public Sub(Class type, Consumer consumer) { + this(0, type, consumer); + } + + @Override + public int compareTo(@NotNull Sub sub) { + return sub.prio == prio ? sub.consumer.getClass().getCanonicalName().compareTo(consumer.getClass().getCanonicalName()) : Integer.compare(sub.prio, prio); + } + +} diff --git a/src/test/java/cc/neckbeard/tinypubsub/tests/AsyncTests.java b/src/test/java/cc/neckbeard/tinypubsub/tests/AsyncTests.java new file mode 100644 index 0000000..dc5284c --- /dev/null +++ b/src/test/java/cc/neckbeard/tinypubsub/tests/AsyncTests.java @@ -0,0 +1,50 @@ +package cc.neckbeard.tinypubsub.tests; + +import cc.neckbeard.tinypubsub.Bus; +import cc.neckbeard.tinypubsub.Sub; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.stream.IntStream; + +@Execution(ExecutionMode.CONCURRENT) +class AsyncTests { + + int runs = 0; + + Sub a = new Sub<>(0, BooleanEvent.class, e -> runs++); + Sub b = new Sub<>(1, BooleanEvent.class, e -> runs++); + Sub c = new Sub<>(2 , BooleanEvent.class, e -> runs++); + + @Test + @DisplayName("unreg while pub") + void unreg() throws InterruptedException { + + Bus bus = new Bus(); + bus.regFields(this); + + Thread t1 = new Thread(() -> IntStream + .range(0, 1000000) + .forEach(i -> bus.pub(new BooleanEvent(true)))); + + Thread t2 = new Thread(() -> IntStream + .range(0, 10000) + .forEach(i -> { + bus.unregFields(this); + bus.reg(a); + })); + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + + Assertions.assertTrue(1000000 * 3 > runs); + + } + +} diff --git a/src/test/java/cc/neckbeard/tinypubsub/tests/BooleanEvent.java b/src/test/java/cc/neckbeard/tinypubsub/tests/BooleanEvent.java new file mode 100644 index 0000000..29694ac --- /dev/null +++ b/src/test/java/cc/neckbeard/tinypubsub/tests/BooleanEvent.java @@ -0,0 +1,13 @@ +package cc.neckbeard.tinypubsub.tests; + +import cc.neckbeard.tinypubsub.Event; + +class BooleanEvent extends Event { + + public boolean bool; + + public BooleanEvent(boolean bool) { + this.bool = bool; + } + +} diff --git a/src/test/java/cc/neckbeard/tinypubsub/tests/BusTests.java b/src/test/java/cc/neckbeard/tinypubsub/tests/BusTests.java new file mode 100644 index 0000000..3f5aa41 --- /dev/null +++ b/src/test/java/cc/neckbeard/tinypubsub/tests/BusTests.java @@ -0,0 +1,39 @@ +package cc.neckbeard.tinypubsub.tests; + +import cc.neckbeard.tinypubsub.Bus; +import cc.neckbeard.tinypubsub.Sub; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Execution(ExecutionMode.CONCURRENT) +class BusTests { + + boolean invoked = false; + + Sub sub = new Sub<>(BooleanEvent.class, e -> invoked = e.bool); + + @Test + void fieldSub() { + Bus bus = new Bus(); + bus.reg(sub); + bus.pub(new BooleanEvent(true)); + Assertions.assertTrue(invoked); + } + + + + @Test + void methodSub() { + AtomicBoolean invoked = new AtomicBoolean(false); + Sub sub = new Sub<>(BooleanEvent.class, e -> invoked.set(e.bool)); + Bus bus = new Bus(); + bus.reg(sub); + bus.pub(new BooleanEvent(true)); + Assertions.assertTrue(invoked.get()); + } + +} diff --git a/src/test/java/cc/neckbeard/tinypubsub/tests/RegTest.java b/src/test/java/cc/neckbeard/tinypubsub/tests/RegTest.java new file mode 100644 index 0000000..94d7ba5 --- /dev/null +++ b/src/test/java/cc/neckbeard/tinypubsub/tests/RegTest.java @@ -0,0 +1,61 @@ +package cc.neckbeard.tinypubsub.tests; + +import cc.neckbeard.tinypubsub.Bus; +import cc.neckbeard.tinypubsub.Sub; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +@Execution(ExecutionMode.CONCURRENT) +class RegTest { + + private Sub fieldSub; + + @Test + void regFields() { + AtomicBoolean invoked = new AtomicBoolean(false); + fieldSub = new Sub<>(BooleanEvent.class, e -> invoked.set(e.bool)); + Bus bus = new Bus(); + bus.regFields(this); + bus.pub(new BooleanEvent(true)); + Assertions.assertTrue(invoked.get()); + bus.unregFields(this); + bus.pub(new BooleanEvent(false)); + Assertions.assertTrue(invoked.get()); + } + + @Test + void regUnique() { + AtomicInteger invoked = new AtomicInteger(0); + Sub sub1 = new Sub<>(BooleanEvent.class, e -> invoked.set(invoked.get() + 1)); + Sub sub2 = new Sub<>(BooleanEvent.class, e -> invoked.set(invoked.get() + 1)); + Sub sub3 = new Sub<>(BooleanEvent.class, e -> invoked.set(invoked.get() + 1)); + Bus bus = new Bus(); + bus.reg(sub1); + bus.reg(sub2); + bus.reg(sub2); + bus.reg(sub3); + bus.reg(sub3); + bus.reg(sub3); + bus.pub(new BooleanEvent(true)); + Assertions.assertEquals(3, invoked.get()); + } + + @Test + void reg() { + AtomicBoolean invoked = new AtomicBoolean(false); + Sub sub = new Sub<>(BooleanEvent.class, e -> invoked.set(e.bool)); + Bus bus = new Bus(); + bus.reg(sub); + bus.pub(new BooleanEvent(true)); + Assertions.assertTrue(invoked.get()); + bus.unreg(sub); + bus.pub(new BooleanEvent(false)); + Assertions.assertTrue(invoked.get()); + } + +} diff --git a/src/test/java/cc/neckbeard/tinypubsub/tests/SortingTests.java b/src/test/java/cc/neckbeard/tinypubsub/tests/SortingTests.java new file mode 100644 index 0000000..e914972 --- /dev/null +++ b/src/test/java/cc/neckbeard/tinypubsub/tests/SortingTests.java @@ -0,0 +1,37 @@ +package cc.neckbeard.tinypubsub.tests; + +import cc.neckbeard.tinypubsub.Sub; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.stream.IntStream; + +@Execution(ExecutionMode.CONCURRENT) +class SortingTests { + + @Test + void shuffle() { + IntStream + .range(0, 100) + .forEach(v -> { + List> expected = new ArrayList<>(); + expected.add(new Sub<>(2, BooleanEvent.class, e -> {})); + expected.add(new Sub<>(1, BooleanEvent.class, e -> {})); + expected.add(new Sub<>(0, BooleanEvent.class, e -> {})); + expected.add(new Sub<>(-1, BooleanEvent.class, e -> {})); + List> random = new ArrayList<>(expected); + Collections.shuffle(random); + List> sorted = new ArrayList<>(new ConcurrentSkipListSet<>(random)); + IntStream + .range(0, 4) + .forEach(i -> Assertions.assertEquals(expected.get(i), sorted.get(i))); + }); + } + +}