Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2025 Split Software, Inc.
Copyright © 2026 Split Software, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
4 changes: 2 additions & 2 deletions api/src/main/java/io/split/android/client/SplitClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.util.Map;

import io.split.android.client.attributes.AttributesManager;
import io.split.android.client.events.SdkEventListener;
import io.split.android.client.events.SplitEventListener;
import io.split.android.client.events.SplitEvent;
import io.split.android.client.events.SplitEventTask;

Expand Down Expand Up @@ -214,7 +214,7 @@ public interface SplitClient extends AttributesManager {
*
* @param listener the event listener to register. Must not be null.
*/
void addEventListener(@NonNull SdkEventListener listener);
void addEventListener(@NonNull SplitEventListener listener);

/**
* Enqueue a new event to be sent to Split data collection services.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* });
* }</pre>
*/
public abstract class SdkEventListener {
public abstract class SplitEventListener {

/**
* Called when SDK_READY event occurs, executed on a background thread.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* </ul>
* <p>
* For events with metadata (like SDK_UPDATE or SDK_READY_FROM_CACHE), use
* {@link SdkEventListener} instead for type-safe metadata access.
* {@link SplitEventListener} instead for type-safe metadata access.
* <p>
* Example usage:
* <pre>{@code
Expand Down
3 changes: 0 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import com.vanniktech.maven.publish.AndroidFusedLibrary
import org.gradle.api.publish.maven.MavenPublication

buildscript {
repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class EventsManagerCoordinator implements ISplitEventsManager, EventsMana

private final ConcurrentMap<Key, ISplitEventsManager> mManagers = new ConcurrentHashMap<>();
private final Set<SplitInternalEvent> mTriggered = Collections.newSetFromMap(new ConcurrentHashMap<SplitInternalEvent, Boolean>());
private final ConcurrentMap<SplitInternalEvent, EventMetadata> mTriggeredMetadata = new ConcurrentHashMap<>();
private final Object mEventLock = new Object();

/**
Expand Down Expand Up @@ -75,6 +76,9 @@ public void notifyInternalEvent(SplitInternalEvent internalEvent, @Nullable Even

synchronized (mEventLock) {
mTriggered.add(internalEvent);
if (metadata != null) {
mTriggeredMetadata.put(internalEvent, metadata);
}

for (ISplitEventsManager manager : mManagers.values()) {
manager.notifyInternalEvent(internalEvent, metadata);
Expand Down Expand Up @@ -123,7 +127,7 @@ public void unregisterEventsManager(Key key) {
private void propagateTriggeredEvents(ISplitEventsManager splitEventsManager) {
synchronized (mEventLock) {
for (SplitInternalEvent event : mTriggered) {
splitEventsManager.notifyInternalEvent(event, null);
splitEventsManager.notifyInternalEvent(event, mTriggeredMetadata.get(event));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public interface ListenableEventsManager {

void register(SplitEvent event, SplitEventTask task);

void registerEventListener(SdkEventListener listener);
void registerEventListener(SplitEventListener listener);

boolean eventAlreadyTriggered(SplitEvent event);
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void register(SplitEvent event, SplitEventTask task) {
}

@Override
public void registerEventListener(SdkEventListener listener) {
public void registerEventListener(SplitEventListener listener) {
requireNonNull(listener);

// Register SDK_READY handlers (bg + main)
Expand Down Expand Up @@ -190,15 +190,15 @@ private EventHandler<SplitEvent, EventMetadata> createMainThreadHandler(final Sp
}

// SdkEventListener handlers for SDK_READY
private EventHandler<SplitEvent, EventMetadata> createReadyBackgroundHandler(final SdkEventListener listener) {
private EventHandler<SplitEvent, EventMetadata> createReadyBackgroundHandler(final SplitEventListener listener) {
return (event, metadata) -> {
SplitClient client = mResources.getSplitClient();
SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata);
executeMethod(() -> listener.onReady(client, typedMetadata));
};
}

private EventHandler<SplitEvent, EventMetadata> createReadyMainThreadHandler(final SdkEventListener listener) {
private EventHandler<SplitEvent, EventMetadata> createReadyMainThreadHandler(final SplitEventListener listener) {
return (event, metadata) -> {
SplitClient client = mResources.getSplitClient();
SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata);
Expand All @@ -207,15 +207,15 @@ private EventHandler<SplitEvent, EventMetadata> createReadyMainThreadHandler(fin
}

// SdkEventListener handlers for SDK_UPDATE
private EventHandler<SplitEvent, EventMetadata> createUpdateBackgroundHandler(final SdkEventListener listener) {
private EventHandler<SplitEvent, EventMetadata> createUpdateBackgroundHandler(final SplitEventListener listener) {
return (event, metadata) -> {
SplitClient client = mResources.getSplitClient();
SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata);
executeMethod(() -> listener.onUpdate(client, typedMetadata));
};
}

private EventHandler<SplitEvent, EventMetadata> createUpdateMainThreadHandler(final SdkEventListener listener) {
private EventHandler<SplitEvent, EventMetadata> createUpdateMainThreadHandler(final SplitEventListener listener) {
return (event, metadata) -> {
SplitClient client = mResources.getSplitClient();
SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata);
Expand All @@ -224,15 +224,15 @@ private EventHandler<SplitEvent, EventMetadata> createUpdateMainThreadHandler(fi
}

// SdkEventListener handlers for SDK_READY_FROM_CACHE
private EventHandler<SplitEvent, EventMetadata> createReadyFromCacheBackgroundHandler(final SdkEventListener listener) {
private EventHandler<SplitEvent, EventMetadata> createReadyFromCacheBackgroundHandler(final SplitEventListener listener) {
return (event, metadata) -> {
SplitClient client = mResources.getSplitClient();
SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata);
executeMethod(() -> listener.onReadyFromCache(client, typedMetadata));
};
}

private EventHandler<SplitEvent, EventMetadata> createReadyFromCacheMainThreadHandler(final SdkEventListener listener) {
private EventHandler<SplitEvent, EventMetadata> createReadyFromCacheMainThreadHandler(final SplitEventListener listener) {
return (event, metadata) -> {
SplitClient client = mResources.getSplitClient();
SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ static EventsManagerConfig<SplitEvent, SplitInternalEvent> create() {
.executionLimit(SplitEvent.SDK_READY_TIMED_OUT, 1)
.executionLimit(SplitEvent.SDK_UPDATE, -1) // unlimited

// Metadata sources
.metadataSource(SplitEvent.SDK_READY, SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)
// Cache path: if SDK_READY_FROM_CACHE fired because cache was loaded, use storage load metadata.
.metadataSource(SplitEvent.SDK_READY_FROM_CACHE, cacheGroup,
SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE)
// Sync path: if SDK_READY_FROM_CACHE fired alongside SDK_READY, use sync completion metadata.
.metadataSource(SplitEvent.SDK_READY_FROM_CACHE, syncGroup,
SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)

.build();
}
}
5 changes: 5 additions & 0 deletions events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Events are configured using `EventsManagerConfig.Builder`:
- **`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)
- **`metadataSource(external, internal)`**: For `requireAll`, selects the internal event whose metadata will be delivered
- **`metadataSource(external, Set<internal>, internal)`**: For `requireAny` groups, selects the metadata source per group

## Topological Sort for Evaluation Order

Expand All @@ -31,6 +33,9 @@ The events system uses **topological sorting** to determine the order in which e
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.
4. **Metadata Selection**: When an external event fires, metadata is resolved from the configured source event:
- `requireAll`: use the configured source internal event
- `requireAny`: use the source configured for the specific group that completed

### Why It's Necessary

Expand Down
62 changes: 60 additions & 2 deletions events/src/main/java/io/harness/events/EventsManagerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public final class EventsManagerConfig<E, I> {
private final Map<E, Set<E>> mSuppressedBy;
// Execution policy: max executions per external event (-1 = unlimited)
private final Map<E, Integer> mExecutionLimits;
// Metadata source for requireAll events
private final Map<E, I> mRequireAllMetadataSource;
// Metadata source for requireAny groups
private final Map<E, Map<Set<I>, I>> mRequireAnyMetadataSource;
// Topologically sorted evaluation order (prerequisites and suppressors come before dependents)
private final List<E> mEvaluationOrder;

Expand All @@ -43,7 +47,9 @@ private EventsManagerConfig(Map<E, Set<I>> requireAll,
Map<E, Set<Set<I>>> requireAny,
Map<E, Set<E>> prerequisites,
Map<E, Set<E>> suppressedBy,
Map<E, Integer> executionLimits) {
Map<E, Integer> executionLimits,
Map<E, I> requireAllMetadataSource,
Map<E, Map<Set<I>, I>> requireAnyMetadataSource) {
mRequireAll = requireAll == null
? Collections.emptyMap()
: Collections.unmodifiableMap(new HashMap<>(requireAll));
Expand All @@ -59,12 +65,20 @@ private EventsManagerConfig(Map<E, Set<I>> requireAll,
mExecutionLimits = executionLimits == null
? Collections.emptyMap()
: Collections.unmodifiableMap(new HashMap<>(executionLimits));
mRequireAllMetadataSource = requireAllMetadataSource == null
? Collections.emptyMap()
: Collections.unmodifiableMap(new HashMap<>(requireAllMetadataSource));
mRequireAnyMetadataSource = requireAnyMetadataSource == null
? Collections.emptyMap()
: Collections.unmodifiableMap(new HashMap<>(requireAnyMetadataSource));

mEvaluationOrder = computeEvaluationOrder();
}

public static <I, E> EventsManagerConfig<E, I> empty() {
return new EventsManagerConfig<>(Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptyMap(),
Expand Down Expand Up @@ -107,6 +121,16 @@ public Map<E, Integer> getExecutionLimits() {
return mExecutionLimits;
}

@NotNull
public Map<E, I> getRequireAllMetadataSource() {
return mRequireAllMetadataSource;
}

@NotNull
public Map<E, Map<Set<I>, I>> getRequireAnyMetadataSource() {
return mRequireAnyMetadataSource;
}

@NotNull
public List<E> getEvaluationOrder() {
return mEvaluationOrder;
Expand Down Expand Up @@ -135,6 +159,8 @@ public static final class Builder<E, I> {
private final Map<E, Set<E>> mPrerequisites = new HashMap<>();
private final Map<E, Set<E>> mSuppressedBy = new HashMap<>();
private final Map<E, Integer> mExecutionLimits = new HashMap<>();
private final Map<E, I> mRequireAllMetadataSource = new HashMap<>();
private final Map<E, Map<Set<I>, I>> mRequireAnyMetadataSource = new HashMap<>();

private Builder() {
}
Expand Down Expand Up @@ -242,6 +268,36 @@ public Builder<E, I> executionLimit(E externalEvent, int limit) {
return this;
}

/**
* Sets the metadata source for a requireAll external event.
*
* @param externalEvent the external event
* @param sourceEvent the internal event whose metadata should be used
* @return this builder
*/
public Builder<E, I> metadataSource(E externalEvent, I sourceEvent) {
mRequireAllMetadataSource.put(externalEvent, sourceEvent);
return this;
}

/**
* Sets the metadata source for a requireAny group.
*
* @param externalEvent the external event
* @param group the internal event group
* @param sourceEvent the internal event whose metadata should be used
* @return this builder
*/
public Builder<E, I> metadataSource(E externalEvent, Set<I> group, I sourceEvent) {
Map<Set<I>, I> groupSources = mRequireAnyMetadataSource.get(externalEvent);
if (groupSources == null) {
groupSources = new HashMap<>();
mRequireAnyMetadataSource.put(externalEvent, groupSources);
}
groupSources.put(new HashSet<>(group), sourceEvent);
return this;
}

/**
* Builds the EventsManagerConfig.
*
Expand All @@ -253,7 +309,9 @@ public EventsManagerConfig<E, I> build() {
mRequireAny.isEmpty() ? null : mRequireAny,
mPrerequisites.isEmpty() ? null : mPrerequisites,
mSuppressedBy.isEmpty() ? null : mSuppressedBy,
mExecutionLimits.isEmpty() ? null : mExecutionLimits
mExecutionLimits.isEmpty() ? null : mExecutionLimits,
mRequireAllMetadataSource.isEmpty() ? null : mRequireAllMetadataSource,
mRequireAnyMetadataSource.isEmpty() ? null : mRequireAnyMetadataSource
);
}
}
Expand Down
Loading
Loading