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 .github/workflows/ci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ concurrency:
jobs:
maven-policy:
name: Shared Maven policy
uses: HauntedMC/HauntedPlatform/.github/workflows/maven-ci.yml@v1.2.0
uses: HauntedMC/HauntedPlatform/.github/workflows/maven-ci.yml@v1.3.0
with:
maven-command: ./mvnw -U -B -ntp -DskipTests verify
secrets:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package nl.hauntedmc.dataprovider.api;

import nl.hauntedmc.dataprovider.api.observation.DataProviderObserver;
import nl.hauntedmc.dataprovider.api.orm.ORMContext;
import nl.hauntedmc.dataprovider.database.DatabaseProvider;
import nl.hauntedmc.dataprovider.database.DatabaseType;
Expand All @@ -19,6 +20,18 @@ default DataProviderAPI forPlugin(Object platformPlugin) {
throw new UnsupportedOperationException("This DataProvider API does not support plugin binding.");
}

/**
* Returns a facade that reports its operations to the supplied vendor-neutral observer.
*
* <p>The observer remains scoped to this facade, its child scopes, and handles obtained from it;
* it is never installed globally. Implementations predating observation support may return this
* facade unchanged.</p>
*/
default DataProviderAPI withObserver(DataProviderObserver observer) {
Objects.requireNonNull(observer, "DataProvider observer cannot be null.");
return this;
}

/**
* Creates an ORM context using the platform administrator's configured schema mode.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package nl.hauntedmc.dataprovider.api.observation;

/**
* One in-flight DataProvider operation observed through {@link DataProviderObserver}.
*
* <p>Exactly one terminal callback is issued by the DataProvider runtime for an observation it
* successfully starts. Implementations must be thread-safe because asynchronous operations may
* complete on a backend worker thread rather than the thread that started the observation.</p>
*/
public interface DataProviderObservation {

/** Called when the observed operation completes successfully. */
void succeeded();

/** Called when the observed operation completes exceptionally or throws. */
void failed(Throwable failure);

/** Returns a reusable observation that ignores all terminal callbacks. */
static DataProviderObservation noop() {
return NoopDataProviderObservation.INSTANCE;
}
}

final class NoopDataProviderObservation implements DataProviderObservation {

static final NoopDataProviderObservation INSTANCE = new NoopDataProviderObservation();

private NoopDataProviderObservation() {
}

@Override
public void succeeded() {
// Intentionally empty.
}

@Override
public void failed(Throwable failure) {
// Intentionally empty.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package nl.hauntedmc.dataprovider.api.observation;

/**
* Vendor-neutral hook for observing plugin-scoped DataProvider operations.
*
* <p>The runtime invokes observers synchronously when an operation starts. The returned observation
* receives its terminal callback when that operation completes; asynchronous operations may finish
* on a backend worker thread. Implementations should therefore be thread-safe and non-blocking.</p>
*
* <p>Observer failures are isolated by the DataProvider runtime and never change the outcome of the
* underlying data operation.</p>
*/
@FunctionalInterface
public interface DataProviderObserver {

/** Starts one operation observation. Implementations should return a non-null handle. */
DataProviderObservation start(DataProviderOperationContext context);

/** Returns the reusable no-op observer used by the uninstrumented fast path. */
static DataProviderObserver noop() {
return NoopDataProviderObserver.INSTANCE;
}
}

final class NoopDataProviderObserver implements DataProviderObserver {

static final NoopDataProviderObserver INSTANCE = new NoopDataProviderObserver();

private NoopDataProviderObserver() {
}

@Override
public DataProviderObservation start(DataProviderOperationContext context) {
return DataProviderObservation.noop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package nl.hauntedmc.dataprovider.api.observation;

import nl.hauntedmc.dataprovider.api.OwnerScope;
import nl.hauntedmc.dataprovider.database.DatabaseType;

import java.util.Objects;

/**
* Stable, payload-free metadata describing one observed DataProvider operation.
*
* <p>The operation name comes from DataProvider's bounded public operation vocabulary, for example
* {@code database.register}, {@code relational.queryForSingle}, or {@code keyvalue.getKey}.
* Connection identifiers, SQL text, keys, destinations, payloads, credentials, and player data are
* deliberately excluded from this contract.</p>
*
* @param pluginId platform-derived plugin identity that owns the API facade
* @param ownerScope public lifecycle owner scope; internal unique registration scopes are never exposed
* @param databaseType backend used by the operation
* @param operation stable DataProvider operation name
*/
public record DataProviderOperationContext(
String pluginId,
OwnerScope ownerScope,
DatabaseType databaseType,
String operation
) {

public DataProviderOperationContext {
Objects.requireNonNull(pluginId, "Plugin id cannot be null.");
Objects.requireNonNull(ownerScope, "Owner scope cannot be null.");
Objects.requireNonNull(databaseType, "Database type cannot be null.");
Objects.requireNonNull(operation, "Operation cannot be null.");
if (pluginId.isBlank()) {
throw new IllegalArgumentException("Plugin id cannot be blank.");
}
if (operation.isBlank()) {
throw new IllegalArgumentException("Operation cannot be blank.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package nl.hauntedmc.dataprovider.core.api;

import nl.hauntedmc.dataprovider.api.observation.DataProviderObservation;
import nl.hauntedmc.dataprovider.api.observation.DataProviderObserver;
import nl.hauntedmc.dataprovider.api.observation.DataProviderOperationContext;

import java.util.Objects;
import java.util.concurrent.CompletionStage;
import java.util.function.Supplier;

/** Isolates optional observation callbacks from the data path. */
final class DataProviderObservations {

private DataProviderObservations() {
}

static boolean isEnabled(DataProviderObserver observer) {
return observer != null && observer != DataProviderObserver.noop();
}

static <T> T observe(
DataProviderObserver observer,
DataProviderOperationContext context,
Supplier<T> operation
) {
Objects.requireNonNull(operation, "Operation cannot be null.");
if (!isEnabled(observer)) {
return operation.get();
}
DataProviderObservation observation = start(observer, context);
try {
T result = operation.get();
succeeded(observation);
return result;
} catch (RuntimeException | Error failure) {
failed(observation, failure);
throw failure;
}
}

static void observe(
DataProviderObserver observer,
DataProviderOperationContext context,
Runnable operation
) {
observe(observer, context, () -> {
operation.run();
return null;
});
}

static Object observeInvocation(
DataProviderObserver observer,
DataProviderOperationContext context,
ThrowingOperation operation
) throws Throwable {
if (!isEnabled(observer)) {
return operation.execute();
}
DataProviderObservation observation = start(observer, context);
final Object result;
try {
result = operation.execute();
} catch (Throwable failure) {
failed(observation, failure);
throw failure;
}
if (result instanceof CompletionStage<?> completionStage) {
try {
completionStage.whenComplete((ignored, failure) -> {
if (failure == null) {
succeeded(observation);
} else {
failed(observation, failure);
}
});
} catch (RuntimeException ignored) {
succeeded(observation);
}
} else {
succeeded(observation);
}
return result;
}

private static DataProviderObservation start(
DataProviderObserver observer,
DataProviderOperationContext context
) {
try {
DataProviderObservation observation = observer.start(context);
return observation == null ? DataProviderObservation.noop() : observation;
} catch (RuntimeException ignored) {
return DataProviderObservation.noop();
}
}

private static void succeeded(DataProviderObservation observation) {
try {
observation.succeeded();
} catch (RuntimeException ignored) {
// Observability must never change a successful data operation into a failure.
}
}

private static void failed(DataProviderObservation observation, Throwable failure) {
try {
observation.failed(failure);
} catch (RuntimeException ignored) {
// Preserve the original data-operation failure.
}
}

@FunctionalInterface
interface ThrowingOperation {
Object execute() throws Throwable;
}
}
Loading
Loading