Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
b6796fc
Version 5.3.2-rc1
gthea Oct 29, 2025
075cb85
WIP fused
gthea Oct 31, 2025
492ee25
WIP standalone / parent module
gthea Oct 31, 2025
3778c1e
Fix
gthea Oct 31, 2025
9038ecc
Consumer rules
gthea Nov 3, 2025
fb4eaeb
Restore
gthea Nov 4, 2025
2de6066
Remove unnecessary code
gthea Nov 4, 2025
4cf4c11
Publish working
gthea Nov 5, 2025
f2481d4
Signing task
gthea Nov 6, 2025
015ef86
Change version; expose SplitLogLevel
gthea Nov 6, 2025
eb4e6fb
Rename internal log level
gthea Nov 6, 2025
dceacb4
Merge branch 'development' into fused
gthea Nov 10, 2025
0fddefc
Restore log level
gthea Nov 10, 2025
4cac101
Update Logger tests
gthea Nov 10, 2025
1e0c0a1
Rename internal modules
gthea Nov 10, 2025
b6eb501
Clean up build.gradle
gthea Nov 10, 2025
4b13de6
Clean up
gthea Nov 10, 2025
2f9315a
Remove .module
gthea Nov 10, 2025
ed961c9
lint fix
gthea Nov 11, 2025
e5801de
Remove project/; refactor Gradle scripts
gthea Nov 12, 2025
d2bf298
Remove group and version in submodules
gthea Nov 12, 2025
33f8395
Scope Kotlin version override
gthea Nov 12, 2025
f0d24fb
Diffuse script enhancements
gthea Nov 14, 2025
89d9c94
Pipeline fix
gthea Nov 14, 2025
9d3831f
Update diffuse script
gthea Nov 14, 2025
9685552
Pull remote branches
gthea Nov 14, 2025
4c95ae3
New script
gthea Nov 14, 2025
3cc2e8e
events module
gthea Nov 14, 2025
e0af4b4
WIP
gthea Nov 14, 2025
cd6ac82
Renaming
gthea Nov 14, 2025
c06fc13
SplitEventsConfig
gthea Nov 18, 2025
99fc8c8
WIP
gthea Nov 20, 2025
3f3a4bb
Merge branch 'FME-11237_baseline' into events-poc
gthea Nov 20, 2025
0361384
Events module and interfaces
gthea Nov 20, 2025
68458c8
Jacoco fixes
gthea Nov 20, 2025
c4788d6
Remove missing dir from Sonar config
gthea Nov 20, 2025
a777140
Remove class
gthea Nov 20, 2025
4b8d1e2
Fix submodule coverage
gthea Nov 25, 2025
8e754ea
Fix coverage collection
gthea Nov 25, 2025
5e8e94f
Simplify sonarqube workflow
gthea Nov 25, 2025
c95de14
Add unregister method
gthea Nov 25, 2025
5da029d
Add libraries property in sonar
gthea Nov 25, 2025
d81f18c
EventsManagerConfig builder
gthea Nov 25, 2025
a605c0e
EventsManagerCore
gthea Nov 25, 2025
d377345
Update Gradle
gthea Nov 25, 2025
8e5050f
Clarification comment
gthea Nov 25, 2025
7c41008
Enhanced sonarqube workflow
gthea Nov 26, 2025
0111c34
Fix issues
gthea Nov 26, 2025
90e2bae
API and events-domain modules
gthea Nov 26, 2025
52b9452
API and Events domain modules
gthea Nov 26, 2025
1c44c5c
EventMetadata implementation
gthea Nov 26, 2025
c7561dd
Modify events-domain README
gthea Nov 26, 2025
0585ebf
Remove stacktrace
gthea Nov 26, 2025
8faefa0
Merge branch 'FME-11237_baseline' into FME-11235
gthea Nov 26, 2025
88cdc6e
Update readme
gthea Nov 26, 2025
c3a4616
Merge branch 'FME-11235' into FME-11235_2
gthea Nov 26, 2025
f9975b2
Cleanup
gthea Nov 26, 2025
0715d65
Clean up
gthea Nov 26, 2025
e1a7566
Cleanup
gthea Nov 26, 2025
41b0921
Use jetbrains annotations for core events module
gthea Nov 26, 2025
172f354
Update jetbrains annotations version
gthea Nov 27, 2025
d61a5c3
Core events delivery functionality
gthea Nov 27, 2025
853b504
Fix javadoc
gthea Nov 27, 2025
0345060
Fix SQ issues
gthea Nov 27, 2025
22d8796
EventsManager provider
gthea Nov 27, 2025
6bbbaab
BG and main thread event delivery
gthea Nov 27, 2025
2c82d0a
Logging
gthea Nov 27, 2025
272e80e
More strict log message check
gthea Nov 27, 2025
67354a2
Remove unnecessary classes
gthea Nov 27, 2025
92236a2
EventsManagersTest
gthea Nov 27, 2025
a5c282b
Move some events files to api and events-domain
gthea Nov 27, 2025
0d6bd0b
Move events classes to events-domain and api modules
gthea Nov 27, 2025
ceeb858
WIP SplitEventsManager
gthea Nov 28, 2025
55d59b4
Fix import
gthea Nov 28, 2025
09424af
Fix unlimited events edge case
gthea Nov 28, 2025
a22aae4
Clean up
gthea Nov 28, 2025
be0c187
Clean up 2
gthea Nov 28, 2025
810e99a
Clean up 3
gthea Nov 28, 2025
043d34d
Events domain fix
gthea Dec 5, 2025
75249b7
Tentative main fix
gthea Dec 5, 2025
d4942e2
Sort events dependencies
gthea Dec 5, 2025
c480ec6
Remove unneccessary if
gthea Dec 5, 2025
84502f3
Merge branch 'FME-11238-split-events_2' into FME-11391-fixes-comp
gthea Dec 5, 2025
6e6d3ff
Delete class
gthea Dec 5, 2025
b481952
Fix bad merge
gthea Dec 5, 2025
d406c49
Fixes
gthea Dec 5, 2025
09f5bbb
Events module readme
gthea Dec 5, 2025
dc205a7
Update test due to prereq
gthea Dec 5, 2025
12dcc26
Fix test
gthea Dec 5, 2025
53bedfb
Remove unused method; update tests
gthea Dec 5, 2025
1659f5c
Update tests
gthea Dec 5, 2025
5a079a6
Refactor and simplification
gthea Dec 9, 2025
773a776
Merge branch 'FME-11391-fixes' into FME-11391-fixes-comp
gthea Dec 9, 2025
9d227fb
Re add all events
gthea Dec 9, 2025
aa270b6
Fixes
gthea Dec 9, 2025
35f97c0
Remove diff
gthea Dec 9, 2025
10d73a5
Clean up
gthea Dec 9, 2025
bdab58f
SQ issues
gthea Dec 9, 2025
5a8f3da
New tests
gthea Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.harness.events.EventHandler;
import io.harness.events.EventsManager;
import io.harness.events.EventsManagers;

import io.split.android.client.api.EventMetadata;
import io.split.android.client.events.executors.SplitEventExecutorResources;
import io.split.android.client.events.executors.SplitEventExecutorResourcesImpl;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package io.split.android.client.events;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

import java.util.Set;

import io.harness.events.EventsManagerConfig;

public class SplitEventsManagerConfigFactoryTest {

@Test
public void configIsNotNull() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();
assertNotNull(config);
}

@Test
public void sdkReadyRequiresTargetingRulesSyncCompleteAndMembershipsSyncComplete() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<SplitInternalEvent> requireAll = config.getRequireAll().get(SplitEvent.SDK_READY);
assertNotNull("SDK_READY should have requireAll configuration", requireAll);
assertTrue("SDK_READY should require TARGETING_RULES_SYNC_COMPLETE",
requireAll.contains(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE));
assertTrue("SDK_READY should require MEMBERSHIPS_SYNC_COMPLETE",
requireAll.contains(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE));
assertEquals("SDK_READY should require exactly 2 events", 2, requireAll.size());
}

@Test
public void sdkReadyHasPrerequisiteSdkReadyFromCache() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<SplitEvent> prerequisites = config.getPrerequisites().get(SplitEvent.SDK_READY);
assertNotNull("SDK_READY should have prerequisites", prerequisites);
assertTrue("SDK_READY should require SDK_READY_FROM_CACHE as prerequisite",
prerequisites.contains(SplitEvent.SDK_READY_FROM_CACHE));
}

@Test
public void sdkReadyHasExecutionLimitOne() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_READY);
assertNotNull("SDK_READY should have execution limit", limit);
assertEquals("SDK_READY should fire at most once", 1, (int) limit);
}

@Test
public void sdkReadyFromCacheHasOrOfAndsConfiguration() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<Set<SplitInternalEvent>> requireAnyGroups = config.getRequireAny().get(SplitEvent.SDK_READY_FROM_CACHE);
assertNotNull("SDK_READY_FROM_CACHE should have requireAny configuration", requireAnyGroups);
assertEquals("SDK_READY_FROM_CACHE should have 2 groups (cache and sync)", 2, requireAnyGroups.size());

boolean hasCacheGroup = false;
boolean hasSyncGroup = false;
for (Set<SplitInternalEvent> group : requireAnyGroups) {
if (group.size() == 4 &&
group.contains(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE) &&
group.contains(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE) &&
group.contains(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE) &&
group.contains(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE)) {
hasCacheGroup = true;
}
if (group.size() == 2 &&
group.contains(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE) &&
group.contains(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE)) {
hasSyncGroup = true;
}
}
assertTrue("SDK_READY_FROM_CACHE should have cache group", hasCacheGroup);
assertTrue("SDK_READY_FROM_CACHE should have sync group", hasSyncGroup);
}

@Test
public void sdkReadyFromCacheHasExecutionLimitOne() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_READY_FROM_CACHE);
assertNotNull("SDK_READY_FROM_CACHE should have execution limit", limit);
assertEquals("SDK_READY_FROM_CACHE should fire at most once", 1, (int) limit);
}
@Test
public void sdkReadyTimedOutRequiresTimeoutReached() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<Set<SplitInternalEvent>> requireAnyGroups = config.getRequireAny().get(SplitEvent.SDK_READY_TIMED_OUT);
assertNotNull("SDK_READY_TIMED_OUT should have requireAny configuration", requireAnyGroups);

boolean hasTimeoutTrigger = false;
for (Set<SplitInternalEvent> group : requireAnyGroups) {
if (group.contains(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED)) {
hasTimeoutTrigger = true;
break;
}
}
assertTrue("SDK_READY_TIMED_OUT should be triggered by SDK_READY_TIMEOUT_REACHED", hasTimeoutTrigger);
}

@Test
public void sdkReadyTimedOutIsSuppressedBySdkReady() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<SplitEvent> suppressors = config.getSuppressedBy().get(SplitEvent.SDK_READY_TIMED_OUT);
assertNotNull("SDK_READY_TIMED_OUT should have suppressors", suppressors);
assertTrue("SDK_READY_TIMED_OUT should be suppressed by SDK_READY",
suppressors.contains(SplitEvent.SDK_READY));
}

@Test
public void sdkReadyTimedOutHasExecutionLimitOne() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_READY_TIMED_OUT);
assertNotNull("SDK_READY_TIMED_OUT should have execution limit", limit);
assertEquals("SDK_READY_TIMED_OUT should fire at most once", 1, (int) limit);
}

@Test
public void sdkUpdateHasCorrectTriggers() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<Set<SplitInternalEvent>> requireAnyGroups = config.getRequireAny().get(SplitEvent.SDK_UPDATE);
assertNotNull("SDK_UPDATE should have requireAny configuration", requireAnyGroups);

// Each trigger should be in its own singleton group
boolean hasSplitsUpdated = false;
boolean hasMySegmentsUpdated = false;
boolean hasMyLargeSegmentsUpdated = false;
boolean hasRuleBasedSegmentsUpdated = false;
boolean hasSplitKilledNotification = false;

for (Set<SplitInternalEvent> group : requireAnyGroups) {
if (group.size() == 1) {
if (group.contains(SplitInternalEvent.SPLITS_UPDATED)) hasSplitsUpdated = true;
if (group.contains(SplitInternalEvent.MY_SEGMENTS_UPDATED)) hasMySegmentsUpdated = true;
if (group.contains(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED)) hasMyLargeSegmentsUpdated = true;
if (group.contains(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED)) hasRuleBasedSegmentsUpdated = true;
if (group.contains(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION)) hasSplitKilledNotification = true;
}
}

assertTrue("SDK_UPDATE should be triggered by SPLITS_UPDATED", hasSplitsUpdated);
assertTrue("SDK_UPDATE should be triggered by MY_SEGMENTS_UPDATED", hasMySegmentsUpdated);
assertTrue("SDK_UPDATE should be triggered by MY_LARGE_SEGMENTS_UPDATED", hasMyLargeSegmentsUpdated);
assertTrue("SDK_UPDATE should be triggered by RULE_BASED_SEGMENTS_UPDATED", hasRuleBasedSegmentsUpdated);
assertTrue("SDK_UPDATE should be triggered by SPLIT_KILLED_NOTIFICATION", hasSplitKilledNotification);
}

@Test
public void sdkUpdateHasPrerequisiteSdkReady() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Set<SplitEvent> prerequisites = config.getPrerequisites().get(SplitEvent.SDK_UPDATE);
assertNotNull("SDK_UPDATE should have prerequisites", prerequisites);
assertTrue("SDK_UPDATE should require SDK_READY as prerequisite",
prerequisites.contains(SplitEvent.SDK_READY));
}

@Test
public void sdkUpdateHasUnlimitedExecutions() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_UPDATE);
assertNotNull("SDK_UPDATE should have execution limit", limit);
assertEquals("SDK_UPDATE should have unlimited executions (-1)", -1, (int) limit);
}

@Test
public void evaluationOrderIsNotEmpty() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

assertNotNull("Evaluation order should not be null", config.getEvaluationOrder());
assertFalse("Evaluation order should not be empty", config.getEvaluationOrder().isEmpty());
}

@Test
public void evaluationOrderContainsAllConfiguredExternalEvents() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

assertTrue("Evaluation order should contain SDK_READY",
config.getEvaluationOrder().contains(SplitEvent.SDK_READY));
assertTrue("Evaluation order should contain SDK_READY_FROM_CACHE",
config.getEvaluationOrder().contains(SplitEvent.SDK_READY_FROM_CACHE));
assertTrue("Evaluation order should contain SDK_READY_TIMED_OUT",
config.getEvaluationOrder().contains(SplitEvent.SDK_READY_TIMED_OUT));
assertTrue("Evaluation order should contain SDK_UPDATE",
config.getEvaluationOrder().contains(SplitEvent.SDK_UPDATE));
}

@Test
public void evaluationOrderHasPrerequisitesBeforeDependents() {
EventsManagerConfig<SplitEvent, SplitInternalEvent> config = SplitEventsManagerConfigFactory.create();

// SDK_READY_FROM_CACHE must come before SDK_READY (prerequisite)
int readyFromCacheIndex = config.getEvaluationOrder().indexOf(SplitEvent.SDK_READY_FROM_CACHE);
int readyIndex = config.getEvaluationOrder().indexOf(SplitEvent.SDK_READY);
assertTrue("SDK_READY_FROM_CACHE should be evaluated before SDK_READY",
readyFromCacheIndex < readyIndex);

// SDK_READY must come before SDK_UPDATE (prerequisite)
int updateIndex = config.getEvaluationOrder().indexOf(SplitEvent.SDK_UPDATE);
assertTrue("SDK_READY should be evaluated before SDK_UPDATE",
readyIndex < updateIndex);
}
}

68 changes: 68 additions & 0 deletions events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,71 @@
This module provides a generic events management system.

Allows the definition of internal and external events interdependencies, as well as registration.

## Core Concepts

### Internal vs External Events

- **Internal Events**: Low-level events triggered by the system (e.g., data loaded, sync completed)
- **External Events**: High-level events exposed to consumers (e.g., SDK_READY, SDK_UPDATE)

### Event Configuration

Events are configured using `EventsManagerConfig.Builder`:

- **`requireAll(external, internal...)`**: External event fires when ALL internal events have occurred
- **`requireAny(external, internal...)`**: External event fires when ANY internal event occurs
- **`requireAny(external, Set<internal>...)`**: OR-of-ANDs pattern; fires when any group is fully satisfied
- **`prerequisite(external, prerequisiteExternal)`**: External event can only fire after the prerequisite external event has fired
- **`suppressedBy(external, suppressorExternal)`**: External event is permanently suppressed if the suppressor external event has already fired
- **`executionLimit(external, limit)`**: Max times the event can fire (-1 = unlimited, 1 = once only)

## Topological Sort for Evaluation Order

The events system uses **topological sorting** to determine the order in which external events are evaluated. This is essential for correctness.

### Evaluation Flow

1. **Internal Event Arrives**: A single internal event can potentially satisfy conditions for multiple external events.
2. **Single-Pass Evaluation**: The system iterates through a pre-computed list of external events (`mEvaluationOrder`).
3. **Order Matters**: This list is topologically sorted so that events with dependencies (prerequisites/suppression) come *after* the events they depend on.

### Why It's Necessary

When a single internal event notification could trigger multiple external events, they must be evaluated in the correct order based on their dependencies.

#### Prerequisite Example

```
SDK_READY_FROM_CACHE ←prerequisite← SDK_READY
```

If both events' conditions are satisfied by the same internal event:

- **Without sort**: If `SDK_READY` is checked first, `prerequisitesSatisfied()` returns `false` because `SDK_READY_FROM_CACHE` hasn't fired yet. `SDK_READY` misses its chance to fire in this cycle.
- **With sort**: `SDK_READY_FROM_CACHE` is evaluated first, fires, then `SDK_READY` sees its prerequisite satisfied and fires—all in one pass.

#### SuppressedBy Example

```
SDK_READY ──suppressedBy──► SDK_READY_TIMED_OUT
```

If both events' conditions are satisfied by the same internal event:

- **Without sort**: If `SDK_READY_TIMED_OUT` is checked first, `isSuppressed()` returns `false` because `SDK_READY` hasn't fired yet. Both events fire incorrectly.
- **With sort**: `SDK_READY` is evaluated first, fires, then `SDK_READY_TIMED_OUT` sees it's suppressed and doesn't fire.

### Implementation Details

The sorting logic is split into:

- **`EventsManagerConfig`**: Holds the raw configuration.
- **`EvaluationOrderComputer`**: Gathers all configured events and builds the dependency graph based on prerequisites and suppressors.
- **`TopologicalSorter`**: A generic utility that performs the DFS-based topological sort with cycle detection.

The topological sort treats both `prerequisite` and `suppressedBy` as dependency edges:
- If A has `prerequisite` B → B must be evaluated before A
- If A is `suppressedBy` B → B must be evaluated before A

**Note:** All configured events are included in the evaluation order, even those without dependencies. Independent events can appear anywhere in the list relative to each other, but always before/after their dependents/dependencies as required.
Loading
Loading