From 4e89fea79f979d1b9ff4a7ae8fc37a63573d6274 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Fri, 27 Feb 2026 17:49:05 -0800 Subject: [PATCH 01/16] Ensure DBOS instance is usable and that DBOS statics exclusively use DBOS instance (where appropriate) Also, hard code handling of internal queue in queue registry and completly remove internal workflow service (used in send) --- .../src/main/java/dev/dbos/transact/DBOS.java | 787 ++++++++++++++---- .../java/dev/dbos/transact/DBOSClient.java | 20 +- .../dbos/transact/context/DBOSContext.java | 5 + .../dbos/transact/execution/DBOSExecutor.java | 61 +- .../dbos/transact/internal/QueueRegistry.java | 10 + .../InternalWorkflowsService.java | 9 - .../InternalWorkflowsServiceImpl.java | 14 - .../dev/dbos/transact/DBOSExtensions.kt | 2 +- 8 files changed, 694 insertions(+), 214 deletions(-) delete mode 100644 transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsService.java delete mode 100644 transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsServiceImpl.java diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index 3de0a453..9b5c4fc6 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -14,8 +14,6 @@ import dev.dbos.transact.internal.QueueRegistry; import dev.dbos.transact.internal.WorkflowRegistry; import dev.dbos.transact.migrations.MigrationManager; -import dev.dbos.transact.tempworkflows.InternalWorkflowsService; -import dev.dbos.transact.tempworkflows.InternalWorkflowsServiceImpl; import dev.dbos.transact.workflow.ForkOptions; import dev.dbos.transact.workflow.ListWorkflowsInput; import dev.dbos.transact.workflow.Queue; @@ -92,8 +90,6 @@ public static class Instance { private DBOSConfig config; - private InternalWorkflowsService internalWorkflowsService; - private final AtomicReference dbosExecutor = new AtomicReference<>(); private Instance() { @@ -155,16 +151,27 @@ private void registerClassWorkflows( return name; } - void registerLifecycleListener(@NonNull DBOSLifecycleListener l) { + /** + * Register a lifecycle listener that receives callbacks when DBOS is launched or shut down + * + * @param listener + */ + public void registerLifecycleListener(@NonNull DBOSLifecycleListener listener) { if (dbosExecutor.get() != null) { throw new IllegalStateException( "Cannot register lifecycle listener after DBOS is launched"); } - lifecycleRegistry.add(l); + lifecycleRegistry.add(listener); } - void registerQueue(@NonNull Queue queue) { + /** + * Register a DBOS queue. This must be called on each queue prior to launch, so that recovery + * has the queue options available. + * + * @param queue `Queue` to register + */ + public void registerQueue(@NonNull Queue queue) { if (dbosExecutor.get() != null) { throw new IllegalStateException("Cannot build a queue after DBOS is launched"); } @@ -172,11 +179,45 @@ void registerQueue(@NonNull Queue queue) { queueRegistry.register(queue); } + /** + * Register a set of DBOS queues. Each queue must be registered prior to launch, so that + * recovery has the queue options available. + * + * @param queues collection of `Queue` instances to register + */ + public void registerQueues(@NonNull Queue... queues) { + for (Queue queue : queues) { + registerQueue(queue); + } + } + + /** + * Register all workflows and steps in the provided class instance + * + * @param The interface type for the instance + * @param interfaceClass The interface class for the workflows + * @param implementation An implementation instance providing the workflow and step function + * code + * @return A proxy, with interface {@literal }, that provides durability for the workflow + * functions + */ public @NonNull T registerWorkflows( @NonNull Class interfaceClass, @NonNull T implementation) { return registerWorkflows(interfaceClass, implementation, ""); } + /** + * Register all workflows and steps in the provided class instance + * + * @param The interface type for the instance + * @param interfaceClass The interface class for the workflows + * @param implementation An implementation instance providing the workflow and step function + * code + * @param instanceName Name of the instance, allowing multiple instances of the same class to be + * registered + * @return A proxy, with interface {@literal }, that provides durability for the workflow + * functions + */ public @NonNull T registerWorkflows( @NonNull Class interfaceClass, @NonNull T implementation, @NonNull String instanceName) { registerClassWorkflows(interfaceClass, implementation, instanceName); @@ -185,20 +226,14 @@ void registerQueue(@NonNull Queue queue) { interfaceClass, implementation, instanceName, () -> this.dbosExecutor.get()); } - private void registerInternals() { - internalWorkflowsService = - registerWorkflows(InternalWorkflowsService.class, new InternalWorkflowsServiceImpl()); - this.registerQueue(new Queue(Constants.DBOS_INTERNAL_QUEUE)); - } - - void clearRegistry() { - workflowRegistry.clear(); - queueRegistry.clear(); - lifecycleRegistry.clear(); - - registerInternals(); - } - + /** + * Registers an {@link AlertHandler} to handle alerts generated by DBOS. This method must be + * called before DBOS is launched; attempting to register an alert handler after launch will + * result in an {@link IllegalStateException}. + * + * @param handler the {@link AlertHandler} instance to register; must not be null + * @throws IllegalStateException if called after DBOS has been launched + */ public void registerAlertHandler(AlertHandler handler) { if (dbosExecutor.get() != null) { throw new IllegalStateException("Cannot set alert handler after DBOS is launched"); @@ -207,11 +242,18 @@ public void registerAlertHandler(AlertHandler handler) { this.alertHandler = handler; } - // package private methods for test purposes + // package private method for test purposes @Nullable DBOSExecutor getDbosExecutor() { return dbosExecutor.get(); } + // package private method for test purposes + void clearRegistry() { + workflowRegistry.clear(); + queueRegistry.clear(); + lifecycleRegistry.clear(); + } + public void setConfig(@NonNull DBOSConfig config) { if (this.config != null) { throw new IllegalStateException("DBOS has already been configured"); @@ -259,6 +301,492 @@ public void shutdown() { } logger.info("DBOS shut down"); } + + DBOSExecutor executor(String caller) { + var exec = dbosExecutor.get(); + if (exec == null) + throw new IllegalStateException( + String.format("Cannot call %s before DBOS is launched", caller)); + return exec; + } + + /** + * Retrieve a queue definition + * + * @param queueName Name of the queue + * @return Queue definition for given `queueName` + */ + public @NonNull Optional getQueue(@NonNull String queueName) { + return executor("getQueue").getQueue(queueName); + } + + /** + * Durable sleep. Use this instead of Thread.sleep, especially in workflows. On restart or + * during recovery the original expected wakeup time is honoured as opposed to sleeping all over + * again. + * + * @param duration amount of time to sleep + */ + public void sleep(@NonNull Duration duration) { + if (!DBOSContext.inWorkflow()) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else if (DBOSContext.inStep()) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else { + executor("sleep").sleep(duration); + } + } + + /** + * Start or enqueue a workflow with a return value + * + * @param Return type of the workflow + * @param Type of checked exception thrown by the workflow, if any + * @param supplier A lambda that calls exactly one workflow function + * @param options Start workflow options + * @return A handle to the enqueued or running workflow + */ + public @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingSupplier supplier, @NonNull StartWorkflowOptions options) { + return executor("startWorkflow").startWorkflow(supplier, options); + } + + /** + * Start or enqueue a workflow with default options + * + * @param Return type of the workflow + * @param Type of checked exception thrown by the workflow, if any + * @param supplier A lambda that calls exactly one workflow function + * @return A handle to the enqueued or running workflow + */ + public @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingSupplier supplier) { + return startWorkflow(supplier, new StartWorkflowOptions()); + } + + /** + * Start or enqueue a workflow with no return value + * + * @param Type of checked exception thrown by the workflow, if any + * @param runnable A lambda that calls exactly one workflow function + * @param options Start workflow options + * @return A handle to the enqueued or running workflow + */ + public @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingRunnable runnable, @NonNull StartWorkflowOptions options) { + return startWorkflow( + () -> { + runnable.execute(); + return null; + }, + options); + } + + /** + * Start or enqueue a workflow with no return value, using default options + * + * @param Type of checked exception thrown by the workflow, if any + * @param runnable A lambda that calls exactly one workflow function + * @return A handle to the enqueued or running workflow + */ + public @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingRunnable runnable) { + return startWorkflow(runnable, new StartWorkflowOptions()); + } + + /** + * Get the result of a workflow, or rethrow the exception thrown by the workflow + * + * @param Return type of the workflow + * @param Checked exception type, if any, thrown by the workflow + * @param workflowId ID of the workflow to retrieve + * @return Return value of the workflow + * @throws E if the workflow threw an exception + */ + public T getResult(@NonNull String workflowId) throws E { + return executor("getResult").getResult(workflowId); + } + + /** + * Get the status of a workflow + * + * @param workflowId ID of the workflow to query + * @return Current workflow status for the provided workflowId, or null. + */ + public @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { + return executor("getWorkflowStatus").getWorkflowStatus(workflowId); + } + + /** + * Send a message to a workflow + * + * @param destinationId recipient of the message + * @param message message to be sent + * @param topic topic to which the message is send + * @param idempotencyKey optional idempotency key for exactly-once send + */ + public void send( + @NonNull String destinationId, + @NonNull Object message, + @Nullable String topic, + @Nullable String idempotencyKey) { + send(destinationId, message, topic, idempotencyKey, null); + } + + /** + * Send a message to a workflow + * + * @param destinationId recipient of the message + * @param message message to be sent + * @param topic topic to which the message is send + */ + public void send( + @NonNull String destinationId, @NonNull Object message, @Nullable String topic) { + send(destinationId, message, topic, null, null); + } + + /** + * Send a message to a workflow with serialization strategy + * + * @param destinationId recipient of the message + * @param message message to be sent + * @param topic topic to which the message is send + * @param idempotencyKey optional idempotency key for exactly-once send + * @param serialization serialization strategy to use (null for default) + */ + public void send( + @NonNull String destinationId, + @NonNull Object message, + @Nullable String topic, + @Nullable String idempotencyKey, + @Nullable SerializationStrategy serialization) { + if (serialization == null) serialization = SerializationStrategy.DEFAULT; + executor("send").send(destinationId, message, topic, idempotencyKey, serialization); + } + + /** + * Get a message sent to a particular topic + * + * @param topic the topic whose message to get + * @param timeout duration after which the call times out + * @return the message if there is one or else null + */ + public @Nullable Object recv(@Nullable String topic, @NonNull Duration timeout) { + return executor("recv").recv(topic, timeout); + } + + /** + * Call within a workflow to publish a key value pair. Uses the workflow's serialization format. + * + * @param key identifier for published data + * @param value data that is published + */ + public void setEvent(@NonNull String key, @NonNull Object value) { + setEvent(key, value, null); + } + + /** + * Call within a workflow to publish a key value pair with a specific serialization strategy. + * + * @param key identifier for published data + * @param value data that is published + * @param serialization serialization strategy to use (null to use workflow's default) + */ + public void setEvent( + @NonNull String key, @NonNull Object value, @Nullable SerializationStrategy serialization) { + // If no explicit serialization specified, use the workflow context's serialization + if (serialization == null) { + serialization = serializationStrategy(); + } + executor("setEvent").setEvent(key, value, serialization); + } + + /** + * Get the data published by a workflow + * + * @param workflowId id of the workflow who data is to be retrieved + * @param key identifies the data + * @param timeout time to wait for data before timing out + * @return the published value or null + */ + public @Nullable Object getEvent( + @NonNull String workflowId, @NonNull String key, @NonNull Duration timeout) { + logger.debug("Received getEvent for {} {}", workflowId, key); + + return executor("getEvent").getEvent(workflowId, key, timeout); + } + + /** + * Run the provided function as a step; this variant is for functions with a return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E + */ + public T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { + + return executor("runStep").runStepInternal(stepfunc, opts, null); + } + + /** + * Run the provided function as a step; this variant is for functions with a return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param name name of the step, for tracing and to record in the system database + * @throws E + */ + public T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { + + return executor("runStep").runStepInternal(stepfunc, new StepOptions(name), null); + } + + /** + * Run the provided function as a step; this variant is for functions with no return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E + */ + public void runStep( + @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { + executor("runStep") + .runStepInternal( + () -> { + stepfunc.execute(); + return null; + }, + opts, + null); + } + + /** + * Run the provided function as a step; this variant is for functions with no return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param name Name of the step, for tracing and recording in the system database + * @throws E + */ + public void runStep( + @NonNull ThrowingRunnable stepfunc, @NonNull String name) throws E { + runStep(stepfunc, new StepOptions(name)); + } + + /** + * Resume a workflow starting from the step after the last complete step + * + * @param Return type of the workflow function + * @param Checked exception thrown by the workflow function, if any + * @param workflowId id of the workflow + * @return A handle to the workflow + */ + public @NonNull WorkflowHandle resumeWorkflow( + @NonNull String workflowId) { + return executor("resumeWorkflow").resumeWorkflow(workflowId); + } + + /*** + * + * Cancel the workflow. After this function is called, the next step (not the + * current one) will not execute + * + * @param workflowId ID of the workflow to cancel + */ + public void cancelWorkflow(@NonNull String workflowId) { + executor("cancelWorkflow").cancelWorkflow(workflowId); + } + + /** + * Fork the workflow. Re-execute with another Id from the step provided. Steps prior to the + * provided step are copied over + * + * @param Return type of the workflow function + * @param Checked exception thrown by the workflow function, if any + * @param workflowId Original workflow Id + * @param startStep Start execution from this step. Prior steps copied over + * @param options {@link ForkOptions} containing forkedWorkflowId, applicationVersion, timeout + * @return handle to the workflow + */ + public @NonNull WorkflowHandle forkWorkflow( + @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { + return executor("forkWorkflow").forkWorkflow(workflowId, startStep, options); + } + + /** + * Fork the workflow. Re-execute with another Id from the step provided. Steps prior to the + * provided step are copied over + * + * @param Return type of the workflow function + * @param Checked exception thrown by the workflow function, if any + * @param workflowId Original workflow Id + * @param startStep Start execution from this step. Prior steps copied over + * @return handle to the workflow + */ + public @NonNull WorkflowHandle forkWorkflow( + @NonNull String workflowId, int startStep) { + return forkWorkflow(workflowId, startStep, new ForkOptions()); + } + + /** + * Deletes a workflow from the system. Does not delete child workflows. + * + * @param workflowId the unique identifier of the workflow to delete. Must not be null. + * @throws IllegalArgumentException if workflowId is null + */ + public void deleteWorkflow(@NonNull String workflowId) { + deleteWorkflow(workflowId, false); + } + + /** + * Deletes a workflow and optionally its child workflows from the system. + * + * @param workflowId the unique identifier of the workflow to delete. Must not be null. + * @param deleteChildren if true, also deletes all child workflows associated with the specified + * workflow; if false, only deletes the specified workflow + * @throws IllegalArgumentException if workflowId is null + */ + public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { + executor("deleteWorkflow").deleteWorkflow(workflowId, deleteChildren); + } + + /** + * Retrieve a handle to a workflow, given its ID. Note that a handle is always returned, whether + * the workflow exists or not; getStatus() can be used to tell the difference + * + * @param Return type of the workflow function + * @param Checked exception thrown by the workflow function, if any + * @param workflowId ID of the workflow to retrieve + * @return Workflow handle for the provided workflow ID + */ + public @NonNull WorkflowHandle retrieveWorkflow( + @NonNull String workflowId) { + return executor("retrieveWorkflow").retrieveWorkflow(workflowId); + } + + /** + * List all workflows + * + * @param input {@link ListWorkflowsInput} parameters to query workflows + * @return a list of workflow status {@link WorkflowStatus} + */ + public @NonNull List listWorkflows(@NonNull ListWorkflowsInput input) { + return executor("listWorkflows").listWorkflows(input); + } + + /** + * List the steps in the workflow + * + * @param workflowId Id of the workflow whose steps to return + * @return list of step information {@link StepInfo} + */ + public @NonNull List listWorkflowSteps(@NonNull String workflowId) { + return executor("listWorkflowSteps").listWorkflowSteps(workflowId); + } + + /** + * Get all workflows registered with DBOS. + * + * @return list of all registered workflow methods + */ + public @NonNull Collection getRegisteredWorkflows() { + return executor("getRegisteredWorkflows").getWorkflows(); + } + + /** + * Get all workflow classes registered with DBOS. + * + * @return list of all class instances containing registered workflow methods + */ + public @NonNull Collection getRegisteredWorkflowInstances() { + return executor("getRegisteredWorkflowInstances").getInstances(); + } + + /** + * Execute a workflow based on registration and arguments. This is expected to be used by + * generic callers, not app code. + * + * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows + * @param args Workflow function arguments + * @param options Execution options, such as ID, queue, and timeout/deadline + * @return WorkflowHandle to the executed workflow + */ + public WorkflowHandle startWorkflow( + RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { + return executor("startWorkflow").startWorkflow(regWorkflow, args, options); + } + + /** + * Get a system database record stored by an external service A unique value is stored per + * combination of service, workflowName, and key + * + * @param service Identity of the service maintaining the record + * @param workflowName Fully qualified name of the workflow + * @param key Key assigned within the service+workflow + * @return Value associated with the service+workflow+key combination + */ + public Optional getExternalState( + String service, String workflowName, String key) { + return executor("getExternalState").getExternalState(service, workflowName, key); + } + + /** + * Insert or update a system database record stored by an external service A timestamped unique + * value is stored per combination of service, workflowName, and key + * + * @param state ExternalState containing the service, workflow, key, and value to store + * @return Value associated with the service+workflow+key combination, in case the stored value + * already had a higher version or timestamp + */ + public ExternalState upsertExternalState(ExternalState state) { + return executor("upsertExternalState").upsertExternalState(state); + } + + /** + * Marks a breaking change within a workflow. Returns true for new workflows (i.e. workflow + * sthat reach this point in the workflow after the breaking change was created) and false for + * old worklows (i.e. workflows that reached this point in the workflow before the breaking + * change was created). The workflow should execute the new code if this method returns true, + * otherwise execute the old code. Note, patching must be enabled in DBOS configuration and this + * method must be called from within a workflow context. + * + * @param patchName the name of the patch to apply + * @return true for workflows started after the breaking change, false for workflows started + * before the breaking change + * @throws RuntimeException if patching is not enabled in DBOS config or if called outside a + * workflow + */ + public boolean patch(@NonNull String patchName) { + return executor("patch").patch(patchName); + } + + /** + * Deprecates a previously applied breaking change patch within a workflow. Safely executes + * workflows containing the patch marker, but does not insert the patch marker into new + * workflows. Always returns true (boolean return gives deprecatePatch the same signature as + * {@link #patch}). Like {@link #patch}, patching must be enabled in DBOS configuration and this + * method must be called from within a workflow context. + * + * @param patchName the name of the patch to deprecate + * @return true (always returns true or throws) + * @throws RuntimeException if patching is not enabled in DBOS config or if called outside a + * workflow + */ + public boolean deprecatePatch(@NonNull String patchName) { + return executor("deprecatePatch").deprecatePatch(patchName); + } } /** @@ -296,11 +824,9 @@ public void shutdown() { * the queue options available. * * @param queue `Queue` to register - * @return input queue */ - public static @NonNull Queue registerQueue(@NonNull Queue queue) { + public static void registerQueue(@NonNull Queue queue) { ensureInstance().registerQueue(queue); - return queue; } /** @@ -310,9 +836,7 @@ public void shutdown() { * @param queues collection of `Queue` instances to register */ public static void registerQueues(@NonNull Queue... queues) { - for (Queue queue : queues) { - registerQueue(queue); - } + ensureInstance().registerQueues(queues); } /** @@ -341,12 +865,12 @@ public static void registerAlertHandler(AlertHandler handler) { * DBOS @DBOSConfig config dbos configuration */ public static synchronized Instance reinitialize(DBOSConfig config) { + // TODO reinit if (config.migrate()) { MigrationManager.runMigrations(config); } var instance = new Instance(); instance.setConfig(config); - instance.registerInternals(); globalInstance = instance; return instance; } @@ -356,9 +880,9 @@ public static synchronized Instance reinitialize(DBOSConfig config) { * startup, before launch. @DBOSConfig config dbos configuration */ public static synchronized Instance configure(DBOSConfig config) { + // TODO config var instance = ensureInstance(); instance.setConfig(config); - instance.registerInternals(); if (config.migrate()) { MigrationManager.runMigrations(config); } @@ -388,24 +912,16 @@ public static void shutdown() { } private static synchronized Instance ensureInstance() { + // TODO ensure instance + + // Simply creating an instance here seems wrong. configure and reinitialize both set config and + // register internals if (globalInstance == null) { globalInstance = new Instance(); } return globalInstance; } - static DBOSExecutor executor(String caller) { - var inst = instance(); - if (inst == null) - throw new IllegalStateException( - String.format("Cannot call %s before DBOS is created", caller)); - var executor = inst.getDbosExecutor(); - if (executor == null) - throw new IllegalStateException( - String.format("Cannot call %s before DBOS is launched", caller)); - return executor; - } - /** * Get the ID of the current running workflow, or `null` if a workflow is not in progress * @@ -438,6 +954,16 @@ public static boolean inStep() { return DBOSContext.inStep(); } + /** + * Get the serialization format of the current workflow context. + * + * @return the serialization format name (e.g., "portable_json", "java_jackson"), or null if not + * in a workflow context or using default serialization + */ + public static @Nullable SerializationStrategy serializationStrategy() { + return DBOSContext.serializationStrategy(); + } + /** * Retrieve a queue definition * @@ -445,7 +971,7 @@ public static boolean inStep() { * @return Queue definition for given `queueName` */ public static @NonNull Optional getQueue(@NonNull String queueName) { - return executor("getQueue").getQueue(queueName); + return ensureInstance().getQueue(queueName); } /** @@ -455,35 +981,7 @@ public static boolean inStep() { * @param duration amount of time to sleep */ public static void sleep(@NonNull Duration duration) { - if (!inWorkflow()) { - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } else if (inStep()) { - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } else { - executor("sleep").sleep(duration); - } - } - - /** - * Start or enqueue a workflow with a return value - * - * @param Return type of the workflow - * @param Type of checked exception thrown by the workflow, if any - * @param supplier A lambda that calls exactly one workflow function - * @param options Start workflow options - * @return A handle to the enqueued or running workflow - */ - public static @NonNull WorkflowHandle startWorkflow( - @NonNull ThrowingSupplier supplier, @NonNull StartWorkflowOptions options) { - return executor("startWorkflow").startWorkflow(supplier, options); + ensureInstance().sleep(duration); } /** @@ -529,6 +1027,20 @@ public static void sleep(@NonNull Duration duration) { return startWorkflow(runnable, new StartWorkflowOptions()); } + /** + * Start or enqueue a workflow with a return value + * + * @param Return type of the workflow + * @param Type of checked exception thrown by the workflow, if any + * @param supplier A lambda that calls exactly one workflow function + * @param options Start workflow options + * @return A handle to the enqueued or running workflow + */ + public static @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingSupplier supplier, @NonNull StartWorkflowOptions options) { + return ensureInstance().startWorkflow(supplier, options); + } + /** * Get the result of a workflow, or rethrow the exception thrown by the workflow * @@ -539,7 +1051,7 @@ public static void sleep(@NonNull Duration duration) { * @throws E if the workflow threw an exception */ public static T getResult(@NonNull String workflowId) throws E { - return executor("getResult").getResult(workflowId); + return ensureInstance().getResult(workflowId); } /** @@ -549,18 +1061,7 @@ public static T getResult(@NonNull String workflowId) t * @return Current workflow status for the provided workflowId, or null. */ public static @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { - return executor("getWorkflowStatus").getWorkflowStatus(workflowId); - } - - /** - * Get the serialization format of the current workflow context. - * - * @return the serialization format name (e.g., "portable_json", "java_jackson"), or null if not - * in a workflow context or using default serialization - */ - public static @Nullable SerializationStrategy getSerialization() { - var ctx = DBOSContextHolder.get(); - return ctx != null ? ctx.getSerialization() : null; + return ensureInstance().getWorkflowStatus(workflowId); } /** @@ -580,41 +1081,33 @@ public static void send( } /** - * Send a message to a workflow with serialization strategy + * Send a message to a workflow * * @param destinationId recipient of the message * @param message message to be sent * @param topic topic to which the message is send - * @param idempotencyKey optional idempotency key for exactly-once send - * @param serialization serialization strategy to use (null for default) */ public static void send( - @NonNull String destinationId, - @NonNull Object message, - @NonNull String topic, - @Nullable String idempotencyKey, - @Nullable SerializationStrategy serialization) { - if (serialization == null) serialization = SerializationStrategy.DEFAULT; - executor("send") - .send( - destinationId, - message, - topic, - instance().internalWorkflowsService, - idempotencyKey, - serialization); + @NonNull String destinationId, @NonNull Object message, @NonNull String topic) { + send(destinationId, message, topic, null, null); } /** - * Send a message to a workflow + * Send a message to a workflow with serialization strategy * * @param destinationId recipient of the message * @param message message to be sent * @param topic topic to which the message is send + * @param idempotencyKey optional idempotency key for exactly-once send + * @param serialization serialization strategy to use (null for default) */ public static void send( - @NonNull String destinationId, @NonNull Object message, @NonNull String topic) { - DBOS.send(destinationId, message, topic, null, null); + @NonNull String destinationId, + @NonNull Object message, + @NonNull String topic, + @Nullable String idempotencyKey, + @Nullable SerializationStrategy serialization) { + ensureInstance().send(destinationId, message, topic, idempotencyKey, serialization); } /** @@ -625,7 +1118,7 @@ public static void send( * @return the message if there is one or else null */ public static @Nullable Object recv(@NonNull String topic, @NonNull Duration timeout) { - return executor("recv").recv(topic, timeout); + return ensureInstance().recv(topic, timeout); } /** @@ -647,11 +1140,7 @@ public static void setEvent(@NonNull String key, @NonNull Object value) { */ public static void setEvent( @NonNull String key, @NonNull Object value, @Nullable SerializationStrategy serialization) { - // If no explicit serialization specified, use the workflow context's serialization - if (serialization == null) { - serialization = getSerialization(); - } - executor("setEvent").setEvent(key, value, serialization); + ensureInstance().setEvent(key, value, serialization); } /** @@ -664,23 +1153,7 @@ public static void setEvent( */ public static @Nullable Object getEvent( @NonNull String workflowId, @NonNull String key, @NonNull Duration timeout) { - logger.debug("Received getEvent for {} {}", workflowId, key); - - return executor("getEvent").getEvent(workflowId, key, timeout); - } - - /** - * Run the provided function as a step; this variant is for functions with a return value - * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param opts step name, and retry options for running the step - * @throws E - */ - public static T runStep( - @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { - - return executor("runStep").runStepInternal(stepfunc, opts, null); + return ensureInstance().getEvent(workflowId, key, timeout); } /** @@ -694,7 +1167,7 @@ public static T runStep( public static T runStep( @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { - return executor("runStep").runStepInternal(stepfunc, new StepOptions(name), null); + return runStep(stepfunc, new StepOptions(name)); } /** @@ -707,14 +1180,12 @@ public static T runStep( */ public static void runStep( @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { - executor("runStep") - .runStepInternal( - () -> { - stepfunc.execute(); - return null; - }, - opts, - null); + runStep( + () -> { + stepfunc.execute(); + return null; + }, + opts); } /** @@ -730,6 +1201,20 @@ public static void runStep( runStep(stepfunc, new StepOptions(name)); } + /** + * Run the provided function as a step; this variant is for functions with a return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E + */ + public static T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { + + return ensureInstance().runStep(stepfunc, opts); + } + /** * Resume a workflow starting from the step after the last complete step * @@ -740,7 +1225,7 @@ public static void runStep( */ public static @NonNull WorkflowHandle resumeWorkflow( @NonNull String workflowId) { - return executor("resumeWorkflow").resumeWorkflow(workflowId); + return ensureInstance().resumeWorkflow(workflowId); } /*** @@ -751,7 +1236,7 @@ public static void runStep( * @param workflowId ID of the workflow to cancel */ public static void cancelWorkflow(@NonNull String workflowId) { - executor("cancelWorkflow").cancelWorkflow(workflowId); + ensureInstance().cancelWorkflow(workflowId); } /** @@ -767,7 +1252,7 @@ public static void cancelWorkflow(@NonNull String workflowId) { */ public static @NonNull WorkflowHandle forkWorkflow( @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { - return executor("forkWorkflow").forkWorkflow(workflowId, startStep, options); + return ensureInstance().forkWorkflow(workflowId, startStep, options); } /** @@ -804,7 +1289,7 @@ public static void deleteWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { - executor("deleteWorkflow").deleteWorkflow(workflowId, deleteChildren); + ensureInstance().deleteWorkflow(workflowId, deleteChildren); } /** @@ -818,7 +1303,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil */ public static @NonNull WorkflowHandle retrieveWorkflow( @NonNull String workflowId) { - return executor("retrieveWorkflow").retrieveWorkflow(workflowId); + return ensureInstance().retrieveWorkflow(workflowId); } /** @@ -828,7 +1313,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return a list of workflow status {@link WorkflowStatus} */ public static @NonNull List listWorkflows(@NonNull ListWorkflowsInput input) { - return executor("listWorkflows").listWorkflows(input); + return ensureInstance().listWorkflows(input); } /** @@ -838,7 +1323,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return list of step information {@link StepInfo} */ public static @NonNull List listWorkflowSteps(@NonNull String workflowId) { - return executor("listWorkflowSteps").listWorkflowSteps(workflowId); + return ensureInstance().listWorkflowSteps(workflowId); } /** @@ -847,7 +1332,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return list of all registered workflow methods */ public static @NonNull Collection getRegisteredWorkflows() { - return executor("getRegisteredWorkflows").getWorkflows(); + return ensureInstance().getRegisteredWorkflows(); } /** @@ -856,7 +1341,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return list of all class instances containing registered workflow methods */ public static @NonNull Collection getRegisteredWorkflowInstances() { - return executor("getRegisteredWorkflowInstances").getInstances(); + return ensureInstance().getRegisteredWorkflowInstances(); } /** @@ -870,7 +1355,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil */ public static WorkflowHandle startWorkflow( RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { - return executor("startWorkflow").startWorkflow(regWorkflow, args, options); + return ensureInstance().startWorkflow(regWorkflow, args, options); } /** @@ -884,7 +1369,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil */ public static Optional getExternalState( String service, String workflowName, String key) { - return executor("getExternalState").getExternalState(service, workflowName, key); + return ensureInstance().getExternalState(service, workflowName, key); } /** @@ -896,7 +1381,7 @@ public static Optional getExternalState( * already had a higher version or timestamp */ public static ExternalState upsertExternalState(ExternalState state) { - return executor("upsertExternalState").upsertExternalState(state); + return ensureInstance().upsertExternalState(state); } /** @@ -914,7 +1399,7 @@ public static ExternalState upsertExternalState(ExternalState state) { * workflow */ public static boolean patch(@NonNull String patchName) { - return executor("patch").patch(patchName); + return ensureInstance().patch(patchName); } /** @@ -930,6 +1415,6 @@ public static boolean patch(@NonNull String patchName) { * workflow */ public static boolean deprecatePatch(@NonNull String patchName) { - return executor("deprecatePatch").deprecatePatch(patchName); + return ensureInstance().deprecatePatch(patchName); } } diff --git a/transact/src/main/java/dev/dbos/transact/DBOSClient.java b/transact/src/main/java/dev/dbos/transact/DBOSClient.java index abd796db..e5c221c6 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOSClient.java +++ b/transact/src/main/java/dev/dbos/transact/DBOSClient.java @@ -595,24 +595,8 @@ public void send( @NonNull String topic, @Nullable String idempotencyKey, @Nullable SendOptions options) { - if (idempotencyKey == null) { - idempotencyKey = UUID.randomUUID().toString(); - } - var workflowId = "%s-%s".formatted(destinationId, idempotencyKey); - - String serializationFormat = - (options != null && options.serialization() != null) - ? options.serialization().formatName() - : null; - - var status = - WorkflowStatusInternal.builder(workflowId, WorkflowState.SUCCESS) - .name("temp_workflow-send-client") - .serialization( - serializationFormat != null ? serializationFormat : SerializationUtil.NATIVE) - .build(); - systemDatabase.initWorkflowStatus(status, null, false, false); - systemDatabase.send(status.workflowId(), 0, destinationId, message, topic, serializationFormat); + var serStrategy = options != null ? options.serialization() : null; + DBOSExecutor.send(destinationId, message, topic, idempotencyKey, serStrategy, systemDatabase); } /** diff --git a/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java b/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java index 58699d08..499e69b1 100644 --- a/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java +++ b/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java @@ -163,4 +163,9 @@ public static boolean inStep() { var ctx = DBOSContextHolder.get(); return ctx == null ? false : ctx.isInStep(); } + + public static SerializationStrategy serializationStrategy() { + var ctx = DBOSContextHolder.get(); + return ctx != null ? ctx.getSerialization() : null; + } } diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java index 1667a97e..0e8127b9 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java @@ -10,7 +10,6 @@ import dev.dbos.transact.context.DBOSContext; import dev.dbos.transact.context.DBOSContextHolder; import dev.dbos.transact.context.WorkflowInfo; -import dev.dbos.transact.context.WorkflowOptions; import dev.dbos.transact.database.ExternalState; import dev.dbos.transact.database.GetWorkflowEventContext; import dev.dbos.transact.database.Result; @@ -27,7 +26,6 @@ import dev.dbos.transact.json.ArgumentCoercion; import dev.dbos.transact.json.DBOSSerializer; import dev.dbos.transact.json.SerializationUtil; -import dev.dbos.transact.tempworkflows.InternalWorkflowsService; import dev.dbos.transact.workflow.ForkOptions; import dev.dbos.transact.workflow.ListWorkflowsInput; import dev.dbos.transact.workflow.Queue; @@ -745,11 +743,35 @@ public void globalTimeout(OffsetDateTime endTime) { } } + // helper function to send a message from outside of a workflow. + // Note, changes to send will make this method unnessary, but we're doing the work in stages + public static void send( + String destinationId, + Object message, + String topic, + String idempotencyKey, + SerializationStrategy serialization, + SystemDatabase systemDatabase) { + if (idempotencyKey == null) { + idempotencyKey = UUID.randomUUID().toString(); + } + var workflowId = "%s-%s".formatted(destinationId, idempotencyKey); + var serializationFormat = + serialization != null ? serialization.formatName() : SerializationUtil.NATIVE; + + var status = + WorkflowStatusInternal.builder(workflowId, WorkflowState.SUCCESS) + .name("temp_workflow-send-client") + .serialization(serializationFormat) + .build(); + systemDatabase.initWorkflowStatus(status, null, false, false); + systemDatabase.send(status.workflowId(), 0, destinationId, message, topic, serializationFormat); + } + public void send( String destinationId, Object message, String topic, - InternalWorkflowsService internalWorkflowsService, String idempotencyKey, SerializationStrategy serialization) { @@ -757,28 +779,25 @@ public void send( if (ctx.isInStep()) { throw new IllegalStateException("DBOS.send() must not be called from within a step."); } + if (!ctx.isInWorkflow()) { - var sendWfid = - idempotencyKey == null ? null : "%s-%s".formatted(destinationId, idempotencyKey); - try (var wfid = new WorkflowOptions(sendWfid).setContext()) { - internalWorkflowsService.sendWorkflow(destinationId, message, topic, serialization); + DBOSExecutor.send( + destinationId, message, topic, idempotencyKey, serialization, systemDatabase); + } else { + if (idempotencyKey != null) { + throw new IllegalArgumentException( + "Invalid call to `DBOS.send` with an idempotency key from within a workflow"); } - return; - } + int stepFunctionId = ctx.getAndIncrementFunctionId(); - if (idempotencyKey != null) { - throw new IllegalArgumentException( - "Invalid call to `DBOS.send` with an idempotency key from within a workflow"); + systemDatabase.send( + ctx.getWorkflowId(), + stepFunctionId, + destinationId, + message, + topic, + serialization.formatName()); } - int stepFunctionId = ctx.getAndIncrementFunctionId(); - - systemDatabase.send( - ctx.getWorkflowId(), - stepFunctionId, - destinationId, - message, - topic, - serialization.formatName()); } /** diff --git a/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java b/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java index f3ff8513..7e3281de 100644 --- a/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java +++ b/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java @@ -1,5 +1,6 @@ package dev.dbos.transact.internal; +import dev.dbos.transact.Constants; import dev.dbos.transact.workflow.Queue; import java.util.List; @@ -10,10 +11,16 @@ public class QueueRegistry { private final ConcurrentHashMap registry = new ConcurrentHashMap<>(); + private final Queue internalQueue = new Queue(Constants.DBOS_INTERNAL_QUEUE); private static final Logger logger = LoggerFactory.getLogger(QueueRegistry.class); public void register(Queue queue) { + if (queue.name().equals(Constants.DBOS_INTERNAL_QUEUE)) { + throw new IllegalArgumentException( + String.format("%s is a reserved queue name", Constants.DBOS_INTERNAL_QUEUE)); + } + if (queue.concurrency() != null && queue.workerConcurrency() != null && queue.workerConcurrency() > queue.concurrency()) { @@ -32,6 +39,9 @@ public void register(Queue queue) { } public Queue get(String queueName) { + if (queueName.equals(Constants.DBOS_INTERNAL_QUEUE)) { + return internalQueue; + } return registry.get(queueName); } diff --git a/transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsService.java b/transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsService.java deleted file mode 100644 index c3e9e07b..00000000 --- a/transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsService.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.dbos.transact.tempworkflows; - -import dev.dbos.transact.workflow.SerializationStrategy; - -public interface InternalWorkflowsService { - - void sendWorkflow( - String destinationId, Object message, String topic, SerializationStrategy serialization); -} diff --git a/transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsServiceImpl.java b/transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsServiceImpl.java deleted file mode 100644 index 4b49f184..00000000 --- a/transact/src/main/java/dev/dbos/transact/tempworkflows/InternalWorkflowsServiceImpl.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.dbos.transact.tempworkflows; - -import dev.dbos.transact.DBOS; -import dev.dbos.transact.workflow.SerializationStrategy; -import dev.dbos.transact.workflow.Workflow; - -public class InternalWorkflowsServiceImpl implements InternalWorkflowsService { - @Workflow(name = "internalSendWorkflow") - public void sendWorkflow( - String destinationId, Object message, String topic, SerializationStrategy serialization) { - - DBOS.send(destinationId, message, topic, null, serialization); - } -} diff --git a/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt b/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt index 29169026..5d174be1 100644 --- a/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt +++ b/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt @@ -27,7 +27,7 @@ inline fun registerWorkflows(implementation: T, instanceName: } @JvmSynthetic -fun registerQueue(queue: Queue): Queue = DBOS.registerQueue(queue) +fun registerQueue(queue: Queue) = DBOS.registerQueue(queue) @JvmSynthetic fun registerLifecycleListener(listener: DBOSLifecycleListener) = DBOS.registerLifecycleListener(listener) From ff0e2b8d3ed829d8f0e910f15a63cda0119b6e18 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 13:06:20 -0800 Subject: [PATCH 02/16] finsih reworking DBOS --- .../src/main/java/dev/dbos/transact/DBOS.java | 414 +++++++++--------- 1 file changed, 199 insertions(+), 215 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index 36370d4a..bb199374 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -2,7 +2,6 @@ import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.DBOSContext; -import dev.dbos.transact.context.DBOSContextHolder; import dev.dbos.transact.database.ExternalState; import dev.dbos.transact.execution.DBOSExecutor; import dev.dbos.transact.execution.DBOSLifecycleListener; @@ -54,13 +53,13 @@ private DBOS() {} private static final Logger logger = LoggerFactory.getLogger(DBOS.class); private static final String version = loadVersionFromResources(); + private static final AtomicReference globalInstance = new AtomicReference<>(); private static @Nullable String loadVersionFromResources() { final String PROPERTIES_FILE = "/dev/dbos/transact/app.properties"; final String VERSION_KEY = "app.version"; Properties props = new Properties(); try (InputStream input = DBOS.class.getResourceAsStream(PROPERTIES_FILE)) { - if (input == null) { logger.warn("Could not find {} resource file", PROPERTIES_FILE); return ""; @@ -71,14 +70,13 @@ private DBOS() {} // Retrieve the version property, defaulting to "unknown" return props.getProperty(VERSION_KEY, ""); - } catch (IOException ex) { logger.error("Error loading version properties", ex); return ""; } } - public static @Nullable String version() { + public static String version() { return version; } @@ -86,69 +84,24 @@ public static class Instance { private final WorkflowRegistry workflowRegistry = new WorkflowRegistry(); private final QueueRegistry queueRegistry = new QueueRegistry(); private final Set lifecycleRegistry = ConcurrentHashMap.newKeySet(); - private AlertHandler alertHandler; - - private DBOSConfig config; - + private final DBOSConfig config; private final AtomicReference dbosExecutor = new AtomicReference<>(); - private Instance() { - DBOSContextHolder.clear(); // CB: Why - } + private AlertHandler alertHandler; - private void registerClassWorkflows( - @NonNull Class interfaceClass, - @NonNull Object implementation, - @Nullable String instanceName) { - Objects.requireNonNull(interfaceClass, "interfaceClass must not be null"); - Objects.requireNonNull(implementation, "implementation must not be null"); - instanceName = Objects.requireNonNullElse(instanceName, ""); - if (!interfaceClass.isInterface()) { - throw new IllegalArgumentException("interfaceClass must be an interface"); - } - if (dbosExecutor.get() != null) { - throw new IllegalStateException("Cannot register workflow after DBOS is launched"); + public Instance(@NonNull DBOSConfig config) { + Objects.requireNonNull(config.appName(), "DBOSConfig.appName must not be null"); + if (config.dataSource() == null) { + Objects.requireNonNull(config.databaseUrl(), "DBOSConfig.databaseUrl must not be null"); + Objects.requireNonNull(config.dbUser(), "DBOSConfig.dbUser must not be null"); + Objects.requireNonNull(config.dbPassword(), "DBOSConfig.dbPassword must not be null"); } - // Use @WorkflowClassName annotation if present, otherwise use the Java class name - WorkflowClassName classNameAnnotation = - implementation.getClass().getAnnotation(WorkflowClassName.class); - String className = - (classNameAnnotation != null && !classNameAnnotation.value().isEmpty()) - ? classNameAnnotation.value() - : implementation.getClass().getName(); - workflowRegistry.register(interfaceClass, implementation, className, instanceName); - - Method[] methods = implementation.getClass().getDeclaredMethods(); - for (Method method : methods) { - Workflow wfAnnotation = method.getAnnotation(Workflow.class); - if (wfAnnotation != null) { - method.setAccessible(true); // In case it's not public - registerWorkflowMethod(wfAnnotation, implementation, className, instanceName, method); - } - } + this.config = config; } - private @NonNull String registerWorkflowMethod( - @NonNull Workflow wfTag, - @NonNull Object target, - @NonNull String className, - @NonNull String instanceName, - @NonNull Method method) { - if (dbosExecutor.get() != null) { - throw new IllegalStateException("Cannot register workflow after DBOS is launched"); - } - - String name = wfTag.name().isEmpty() ? method.getName() : wfTag.name(); - workflowRegistry.register( - className, - name, - target, - instanceName, - method, - wfTag.maxRecoveryAttempts(), - wfTag.serializationStrategy()); - return name; + public String version() { + return DBOS.version(); } /** @@ -226,6 +179,60 @@ public void registerQueues(@NonNull Queue... queues) { interfaceClass, implementation, instanceName, () -> this.dbosExecutor.get()); } + private void registerClassWorkflows( + @NonNull Class interfaceClass, + @NonNull Object implementation, + @Nullable String instanceName) { + Objects.requireNonNull(interfaceClass, "interfaceClass must not be null"); + Objects.requireNonNull(implementation, "implementation must not be null"); + instanceName = Objects.requireNonNullElse(instanceName, ""); + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException("interfaceClass must be an interface"); + } + if (dbosExecutor.get() != null) { + throw new IllegalStateException("Cannot register workflow after DBOS is launched"); + } + + // Use @WorkflowClassName annotation if present, otherwise use the Java class name + WorkflowClassName classNameAnnotation = + implementation.getClass().getAnnotation(WorkflowClassName.class); + String className = + (classNameAnnotation != null && !classNameAnnotation.value().isEmpty()) + ? classNameAnnotation.value() + : implementation.getClass().getName(); + workflowRegistry.register(interfaceClass, implementation, className, instanceName); + + Method[] methods = implementation.getClass().getDeclaredMethods(); + for (Method method : methods) { + Workflow wfAnnotation = method.getAnnotation(Workflow.class); + if (wfAnnotation != null) { + method.setAccessible(true); // In case it's not public + registerWorkflowMethod(wfAnnotation, implementation, className, instanceName, method); + } + } + } + + private void registerWorkflowMethod( + @NonNull Workflow wfTag, + @NonNull Object target, + @NonNull String className, + @NonNull String instanceName, + @NonNull Method method) { + if (dbosExecutor.get() != null) { + throw new IllegalStateException("Cannot register workflow after DBOS is launched"); + } + + String name = wfTag.name().isEmpty() ? method.getName() : wfTag.name(); + workflowRegistry.register( + className, + name, + target, + instanceName, + method, + wfTag.maxRecoveryAttempts(), + wfTag.serializationStrategy()); + } + /** * Registers an {@link AlertHandler} to handle alerts generated by DBOS. This method must be * called before DBOS is launched; attempting to register an alert handler after launch will @@ -247,32 +254,18 @@ public void registerAlertHandler(AlertHandler handler) { return dbosExecutor.get(); } - // package private method for test purposes - void clearRegistry() { - workflowRegistry.clear(); - queueRegistry.clear(); - lifecycleRegistry.clear(); - } - - public void setConfig(@NonNull DBOSConfig config) { - if (this.config != null) { - throw new IllegalStateException("DBOS has already been configured"); - } - - Objects.requireNonNull(config.appName(), "DBOSConfig.appName must not be null"); - if (config.dataSource() == null) { - Objects.requireNonNull(config.databaseUrl(), "DBOSConfig.databaseUrl must not be null"); - Objects.requireNonNull(config.dbUser(), "DBOSConfig.dbUser must not be null"); - Objects.requireNonNull(config.dbPassword(), "DBOSConfig.dbPassword must not be null"); - } - - this.config = config; - } + // // package private method for test purposes + // void clearRegistry() { + // workflowRegistry.clear(); + // queueRegistry.clear(); + // lifecycleRegistry.clear(); + // } + /** + * Launch DBOS, and start recovery. All workflows, queues, and other objects should be + * registered before launch + */ public void launch() { - if (this.config == null) { - throw new IllegalStateException("DBOS must be configured before launch()"); - } var ver = DBOS.version(); logger.info("Launching DBOS {}", ver == null ? "" : "v" + ver); @@ -280,6 +273,10 @@ public void launch() { var executor = new DBOSExecutor(config); if (dbosExecutor.compareAndSet(null, executor)) { + if (config.migrate()) { + MigrationManager.runMigrations(config); + } + executor.start( this, new HashSet<>(this.lifecycleRegistry), @@ -291,6 +288,10 @@ public void launch() { } } + /** + * Shut down DBOS. This method should only be used in test environments, where DBOS is used + * multiple times in the same JVM. + */ public void shutdown() { var current = dbosExecutor.get(); if (current != null) { @@ -302,11 +303,13 @@ public void shutdown() { logger.info("DBOS shut down"); } - DBOSExecutor executor(String caller) { + // helper for methods that can only be called after launch + private DBOSExecutor ensureLaunched(String caller) { var exec = dbosExecutor.get(); - if (exec == null) + if (exec == null) { throw new IllegalStateException( String.format("Cannot call %s before DBOS is launched", caller)); + } return exec; } @@ -317,7 +320,7 @@ DBOSExecutor executor(String caller) { * @return Queue definition for given `queueName` */ public @NonNull Optional getQueue(@NonNull String queueName) { - return executor("getQueue").getQueue(queueName); + return ensureLaunched("getQueue").getQueue(queueName); } /** @@ -341,7 +344,7 @@ public void sleep(@NonNull Duration duration) { Thread.currentThread().interrupt(); } } else { - executor("sleep").sleep(duration); + ensureLaunched("sleep").sleep(duration); } } @@ -356,7 +359,7 @@ public void sleep(@NonNull Duration duration) { */ public @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingSupplier supplier, @NonNull StartWorkflowOptions options) { - return executor("startWorkflow").startWorkflow(supplier, options); + return ensureLaunched("startWorkflow").startWorkflow(supplier, options); } /** @@ -402,6 +405,20 @@ public void sleep(@NonNull Duration duration) { return startWorkflow(runnable, new StartWorkflowOptions()); } + /** + * Execute a workflow based on registration and arguments. This is expected to be used by event + * listeners, not app code. + * + * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows + * @param args Workflow function arguments + * @param options Execution options, such as ID, queue, and timeout/deadline + * @return WorkflowHandle to the executed workflow + */ + public WorkflowHandle startWorkflow( + RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { + return ensureLaunched("startWorkflow").startWorkflow(regWorkflow, args, options); + } + /** * Get the result of a workflow, or rethrow the exception thrown by the workflow * @@ -412,7 +429,7 @@ public void sleep(@NonNull Duration duration) { * @throws E if the workflow threw an exception */ public T getResult(@NonNull String workflowId) throws E { - return executor("getResult").getResult(workflowId); + return ensureLaunched("getResult").getResult(workflowId); } /** @@ -422,7 +439,7 @@ public T getResult(@NonNull String workflowId) throws E * @return Current workflow status for the provided workflowId, or null. */ public @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { - return executor("getWorkflowStatus").getWorkflowStatus(workflowId); + return ensureLaunched("getWorkflowStatus").getWorkflowStatus(workflowId); } /** @@ -469,7 +486,7 @@ public void send( @Nullable String idempotencyKey, @Nullable SerializationStrategy serialization) { if (serialization == null) serialization = SerializationStrategy.DEFAULT; - executor("send").send(destinationId, message, topic, idempotencyKey, serialization); + ensureLaunched("send").send(destinationId, message, topic, idempotencyKey, serialization); } /** @@ -480,7 +497,7 @@ public void send( * @return the message if there is one or else null */ public @Nullable Object recv(@Nullable String topic, @NonNull Duration timeout) { - return executor("recv").recv(topic, timeout); + return ensureLaunched("recv").recv(topic, timeout); } /** @@ -506,7 +523,7 @@ public void setEvent( if (serialization == null) { serialization = serializationStrategy(); } - executor("setEvent").setEvent(key, value, serialization); + ensureLaunched("setEvent").setEvent(key, value, serialization); } /** @@ -521,7 +538,7 @@ public void setEvent( @NonNull String workflowId, @NonNull String key, @NonNull Duration timeout) { logger.debug("Received getEvent for {} {}", workflowId, key); - return executor("getEvent").getEvent(workflowId, key, timeout); + return ensureLaunched("getEvent").getEvent(workflowId, key, timeout); } /** @@ -535,7 +552,7 @@ public void setEvent( public T runStep( @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { - return executor("runStep").runStepInternal(stepfunc, opts, null); + return ensureLaunched("runStep").runStepInternal(stepfunc, opts, null); } /** @@ -548,8 +565,7 @@ public T runStep( */ public T runStep( @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { - - return executor("runStep").runStepInternal(stepfunc, new StepOptions(name), null); + return runStep(stepfunc, new StepOptions(name)); } /** @@ -562,14 +578,12 @@ public T runStep( */ public void runStep( @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { - executor("runStep") - .runStepInternal( - () -> { - stepfunc.execute(); - return null; - }, - opts, - null); + runStep( + () -> { + stepfunc.execute(); + return null; + }, + opts); } /** @@ -595,7 +609,7 @@ public void runStep( */ public @NonNull WorkflowHandle resumeWorkflow( @NonNull String workflowId) { - return executor("resumeWorkflow").resumeWorkflow(workflowId); + return ensureLaunched("resumeWorkflow").resumeWorkflow(workflowId); } /*** @@ -606,7 +620,7 @@ public void runStep( * @param workflowId ID of the workflow to cancel */ public void cancelWorkflow(@NonNull String workflowId) { - executor("cancelWorkflow").cancelWorkflow(workflowId); + ensureLaunched("cancelWorkflow").cancelWorkflow(workflowId); } /** @@ -622,7 +636,7 @@ public void cancelWorkflow(@NonNull String workflowId) { */ public @NonNull WorkflowHandle forkWorkflow( @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { - return executor("forkWorkflow").forkWorkflow(workflowId, startStep, options); + return ensureLaunched("forkWorkflow").forkWorkflow(workflowId, startStep, options); } /** @@ -659,7 +673,7 @@ public void deleteWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { - executor("deleteWorkflow").deleteWorkflow(workflowId, deleteChildren); + ensureLaunched("deleteWorkflow").deleteWorkflow(workflowId, deleteChildren); } /** @@ -673,7 +687,7 @@ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { */ public @NonNull WorkflowHandle retrieveWorkflow( @NonNull String workflowId) { - return executor("retrieveWorkflow").retrieveWorkflow(workflowId); + return ensureLaunched("retrieveWorkflow").retrieveWorkflow(workflowId); } /** @@ -683,7 +697,7 @@ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { * @return a list of workflow status {@link WorkflowStatus} */ public @NonNull List listWorkflows(@NonNull ListWorkflowsInput input) { - return executor("listWorkflows").listWorkflows(input); + return ensureLaunched("listWorkflows").listWorkflows(input); } /** @@ -693,7 +707,7 @@ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { * @return list of step information {@link StepInfo} */ public @NonNull List listWorkflowSteps(@NonNull String workflowId) { - return executor("listWorkflowSteps").listWorkflowSteps(workflowId); + return ensureLaunched("listWorkflowSteps").listWorkflowSteps(workflowId); } /** @@ -702,7 +716,7 @@ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { * @return list of all registered workflow methods */ public @NonNull Collection getRegisteredWorkflows() { - return executor("getRegisteredWorkflows").getWorkflows(); + return ensureLaunched("getRegisteredWorkflows").getWorkflows(); } /** @@ -711,21 +725,7 @@ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { * @return list of all class instances containing registered workflow methods */ public @NonNull Collection getRegisteredWorkflowInstances() { - return executor("getRegisteredWorkflowInstances").getInstances(); - } - - /** - * Execute a workflow based on registration and arguments. This is expected to be used by - * generic callers, not app code. - * - * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows - * @param args Workflow function arguments - * @param options Execution options, such as ID, queue, and timeout/deadline - * @return WorkflowHandle to the executed workflow - */ - public WorkflowHandle startWorkflow( - RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { - return executor("startWorkflow").startWorkflow(regWorkflow, args, options); + return ensureLaunched("getRegisteredWorkflowInstances").getInstances(); } /** @@ -739,7 +739,7 @@ public void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { */ public Optional getExternalState( String service, String workflowName, String key) { - return executor("getExternalState").getExternalState(service, workflowName, key); + return ensureLaunched("getExternalState").getExternalState(service, workflowName, key); } /** @@ -751,7 +751,7 @@ public Optional getExternalState( * already had a higher version or timestamp */ public ExternalState upsertExternalState(ExternalState state) { - return executor("upsertExternalState").upsertExternalState(state); + return ensureLaunched("upsertExternalState").upsertExternalState(state); } /** @@ -769,7 +769,7 @@ public ExternalState upsertExternalState(ExternalState state) { * workflow */ public boolean patch(@NonNull String patchName) { - return executor("patch").patch(patchName); + return ensureLaunched("patch").patch(patchName); } /** @@ -785,10 +785,50 @@ public boolean patch(@NonNull String patchName) { * workflow */ public boolean deprecatePatch(@NonNull String patchName) { - return executor("deprecatePatch").deprecatePatch(patchName); + return ensureLaunched("deprecatePatch").deprecatePatch(patchName); + } + } + + /** + * Initializes the singleton instance of DBOS with config. Should be called once during app + * startup, before launch. @DBOSConfig config dbos configuration + */ + public static void configure(DBOSConfig config) { + if (globalInstance.get() != null) { + throw new IllegalStateException("DBOS is already configured"); + } + + var instance = new DBOS.Instance(config); + var updated = globalInstance.compareAndSet(null, instance); + if (!updated) { + throw new IllegalStateException("DBOS is already configured"); } } + /** + * Unconditionally initializes the singleton instance of DBOS with config, even if one was already + * set. For use in tests that reinitialize DBOS @DBOSConfig config dbos configuration. Package + * private method for test purposes + */ + static void reinitialize(DBOSConfig config) { + var instance = new DBOS.Instance(config); + globalInstance.set(instance); + } + + private static Instance ensureInstance() { + var instance = globalInstance.get(); + if (instance == null) { + throw new IllegalStateException("DBOS instance is not initialized"); + } + return instance; + } + + // package private method for test purposes + static @Nullable DBOSExecutor getDbosExecutor() { + var instance = ensureInstance(); + return instance == null ? null : instance.getDbosExecutor(); + } + /** * Register all workflows and steps in the provided class instance * @@ -860,35 +900,6 @@ public static void registerAlertHandler(AlertHandler handler) { ensureInstance().registerAlertHandler(handler); } - /** - * Reinitializes the singleton instance of DBOS with config. For use in tests that reinitialize - * DBOS @DBOSConfig config dbos configuration - */ - public static synchronized Instance reinitialize(DBOSConfig config) { - // TODO reinit - if (config.migrate()) { - MigrationManager.runMigrations(config); - } - var instance = new Instance(); - instance.setConfig(config); - globalInstance = instance; - return instance; - } - - /** - * Initializes the singleton instance of DBOS with config. Should be called once during app - * startup, before launch. @DBOSConfig config dbos configuration - */ - public static synchronized Instance configure(DBOSConfig config) { - // TODO config - var instance = ensureInstance(); - instance.setConfig(config); - if (config.migrate()) { - MigrationManager.runMigrations(config); - } - return instance; - } - /** * Launch DBOS, and start recovery. All workflows, queues, and other objects should be registered * before launch @@ -902,24 +913,7 @@ public static void launch() { * multiple times in the same JVM. */ public static void shutdown() { - if (globalInstance != null) globalInstance.shutdown(); - } - - private static @Nullable Instance globalInstance = null; - - public static @Nullable Instance instance() { - return globalInstance; - } - - private static synchronized Instance ensureInstance() { - // TODO ensure instance - - // Simply creating an instance here seems wrong. configure and reinitialize both set config and - // register internals - if (globalInstance == null) { - globalInstance = new Instance(); - } - return globalInstance; + ensureInstance().shutdown(); } /** @@ -994,7 +988,7 @@ public static void sleep(@NonNull Duration duration) { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingSupplier supplier) { - return startWorkflow(supplier, new StartWorkflowOptions()); + return ensureInstance().startWorkflow(supplier, new StartWorkflowOptions()); } /** @@ -1007,12 +1001,7 @@ public static void sleep(@NonNull Duration duration) { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingRunnable runnable, @NonNull StartWorkflowOptions options) { - return startWorkflow( - () -> { - runnable.execute(); - return null; - }, - options); + return ensureInstance().startWorkflow(runnable, options); } /** @@ -1024,7 +1013,7 @@ public static void sleep(@NonNull Duration duration) { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingRunnable runnable) { - return startWorkflow(runnable, new StartWorkflowOptions()); + return ensureInstance().startWorkflow(runnable, new StartWorkflowOptions()); } /** @@ -1041,6 +1030,20 @@ public static void sleep(@NonNull Duration duration) { return ensureInstance().startWorkflow(supplier, options); } + /** + * Execute a workflow based on registration and arguments. This is expected to be used by event + * listeners, not app code. + * + * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows + * @param args Workflow function arguments + * @param options Execution options, such as ID, queue, and timeout/deadline + * @return WorkflowHandle to the executed workflow + */ + public static WorkflowHandle startWorkflow( + RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { + return ensureInstance().startWorkflow(regWorkflow, args, options); + } + /** * Get the result of a workflow, or rethrow the exception thrown by the workflow * @@ -1077,7 +1080,7 @@ public static void send( @NonNull Object message, @Nullable String topic, @Nullable String idempotencyKey) { - send(destinationId, message, topic, idempotencyKey, null); + ensureInstance().send(destinationId, message, topic, idempotencyKey); } /** @@ -1089,7 +1092,7 @@ public static void send( */ public static void send( @NonNull String destinationId, @NonNull Object message, @NonNull String topic) { - send(destinationId, message, topic, null, null); + ensureInstance().send(destinationId, message, topic); } /** @@ -1128,7 +1131,7 @@ public static void send( * @param value data that is published */ public static void setEvent(@NonNull String key, @NonNull Object value) { - setEvent(key, value, null); + ensureInstance().setEvent(key, value); } /** @@ -1167,7 +1170,7 @@ public static void setEvent( public static T runStep( @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { - return runStep(stepfunc, new StepOptions(name)); + return ensureInstance().runStep(stepfunc, name); } /** @@ -1180,12 +1183,7 @@ public static T runStep( */ public static void runStep( @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { - runStep( - () -> { - stepfunc.execute(); - return null; - }, - opts); + ensureInstance().runStep(stepfunc, opts); } /** @@ -1198,7 +1196,7 @@ public static void runStep( */ public static void runStep( @NonNull ThrowingRunnable stepfunc, @NonNull String name) throws E { - runStep(stepfunc, new StepOptions(name)); + ensureInstance().runStep(stepfunc, name); } /** @@ -1267,7 +1265,7 @@ public static void cancelWorkflow(@NonNull String workflowId) { */ public static @NonNull WorkflowHandle forkWorkflow( @NonNull String workflowId, int startStep) { - return forkWorkflow(workflowId, startStep, new ForkOptions()); + return ensureInstance().forkWorkflow(workflowId, startStep); } /** @@ -1277,7 +1275,7 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public static void deleteWorkflow(@NonNull String workflowId) { - deleteWorkflow(workflowId, false); + ensureInstance().deleteWorkflow(workflowId); } /** @@ -1344,20 +1342,6 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil return ensureInstance().getRegisteredWorkflowInstances(); } - /** - * Execute a workflow based on registration and arguments. This is expected to be used by generic - * callers, not app code. - * - * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows - * @param args Workflow function arguments - * @param options Execution options, such as ID, queue, and timeout/deadline - * @return WorkflowHandle to the executed workflow - */ - public static WorkflowHandle startWorkflow( - RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { - return ensureInstance().startWorkflow(regWorkflow, args, options); - } - /** * Get a system database record stored by an external service A unique value is stored per * combination of service, workflowName, and key From eaeca73788e3239096af02a994f0a122c9c9e34a Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 13:06:26 -0800 Subject: [PATCH 03/16] update tests --- .../dev/dbos/transact/DBOSTestAccess.java | 16 +++++- .../dev/dbos/transact/client/ClientTest.java | 2 +- .../dbos/transact/client/PgSqlClientTest.java | 2 +- .../dev/dbos/transact/config/ConfigTest.java | 20 +++---- .../dbos/transact/database/MetricsTest.java | 2 +- .../transact/execution/DBOSExecutorTest.java | 57 +++++++++---------- .../transact/execution/LifecycleTest.java | 2 +- .../execution/RecoveryServiceTest.java | 4 +- .../dbos/transact/execution/ScaleTest.java | 2 +- .../execution/SingleExecutionTest.java | 2 +- .../transact/invocation/CustomSchemaTest.java | 3 +- .../invocation/DirectInvocationTest.java | 3 +- .../transact/invocation/MultiInstTest.java | 2 +- .../dbos/transact/invocation/PatchTest.java | 16 +++--- .../invocation/StartWorkflowTest.java | 3 +- .../dev/dbos/transact/issues/Issue218.java | 3 +- .../dev/dbos/transact/json/InteropTest.java | 3 +- .../json/PortableSerializationTest.java | 13 +++-- .../transact/notifications/EventsTest.java | 2 +- .../NotificationServiceTest.java | 3 +- .../transact/queue/PartitionedQueuesTest.java | 3 +- .../dev/dbos/transact/queue/QueuesTest.java | 4 +- .../scheduled/SchedulerServiceTest.java | 2 +- .../dev/dbos/transact/step/StepsTest.java | 3 +- .../transact/workflow/AsyncWorkflowTest.java | 3 +- .../dev/dbos/transact/workflow/ForkTest.java | 2 +- .../workflow/GarbageCollectionTest.java | 2 +- .../transact/workflow/ListWorkflowsTest.java | 3 +- .../workflow/QueueChildWorkflowTest.java | 3 +- .../transact/workflow/SyncWorkflowTest.java | 3 +- .../dbos/transact/workflow/TimeoutTest.java | 2 +- .../transact/workflow/UnifiedProxyTest.java | 3 +- .../transact/workflow/WorkflowMgmtTest.java | 3 +- 33 files changed, 110 insertions(+), 86 deletions(-) diff --git a/transact/src/test/java/dev/dbos/transact/DBOSTestAccess.java b/transact/src/test/java/dev/dbos/transact/DBOSTestAccess.java index f945e37f..7106948a 100644 --- a/transact/src/test/java/dev/dbos/transact/DBOSTestAccess.java +++ b/transact/src/test/java/dev/dbos/transact/DBOSTestAccess.java @@ -1,5 +1,6 @@ package dev.dbos.transact; +import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.execution.DBOSExecutor; import dev.dbos.transact.execution.DBOSExecutorTestAccess; @@ -8,14 +9,23 @@ // Helper class to retrieve DBOS internals via package private methods public class DBOSTestAccess { + + public static DBOSExecutor getDbosExecutor(DBOS.Instance instance) { + return instance.getDbosExecutor(); + } + public static DBOSExecutor getDbosExecutor() { - return DBOS.instance().getDbosExecutor(); + return DBOS.getDbosExecutor(); } - public static void clearRegistry() { - DBOS.instance().clearRegistry(); + public static void reinitialize(DBOSConfig config) { + DBOS.reinitialize(config); } + // public static void clearRegistry() { + // DBOS.instance().clearRegistry(); + // } + public static SystemDatabase getSystemDatabase() { var exec = getDbosExecutor(); return DBOSExecutorTestAccess.getSystemDatabase(exec); diff --git a/transact/src/test/java/dev/dbos/transact/client/ClientTest.java b/transact/src/test/java/dev/dbos/transact/client/ClientTest.java index 2c6f49ff..a823b178 100644 --- a/transact/src/test/java/dev/dbos/transact/client/ClientTest.java +++ b/transact/src/test/java/dev/dbos/transact/client/ClientTest.java @@ -45,7 +45,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); DBOS.registerQueue(new Queue("testQueue")); service = DBOS.registerWorkflows(ClientService.class, new ClientServiceImpl()); DBOS.launch(); diff --git a/transact/src/test/java/dev/dbos/transact/client/PgSqlClientTest.java b/transact/src/test/java/dev/dbos/transact/client/PgSqlClientTest.java index f5f2d8f0..6a3358bd 100644 --- a/transact/src/test/java/dev/dbos/transact/client/PgSqlClientTest.java +++ b/transact/src/test/java/dev/dbos/transact/client/PgSqlClientTest.java @@ -53,7 +53,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); DBOS.registerQueue(new Queue("testQueue")); service = DBOS.registerWorkflows(ClientService.class, new ClientServiceImpl()); DBOS.launch(); diff --git a/transact/src/test/java/dev/dbos/transact/config/ConfigTest.java b/transact/src/test/java/dev/dbos/transact/config/ConfigTest.java index 56c2f365..5aedb644 100644 --- a/transact/src/test/java/dev/dbos/transact/config/ConfigTest.java +++ b/transact/src/test/java/dev/dbos/transact/config/ConfigTest.java @@ -56,7 +56,7 @@ public void configOverridesEnvAppVerAndExecutor() throws Exception { .withAppVersion("test-app-version") .withExecutorId("test-executor-id"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); @@ -81,7 +81,7 @@ public void envAppVerAndExecutor() throws Exception { .withDbUser("postgres") .withDbPassword(System.getenv("PGPASSWORD")); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); @@ -109,7 +109,7 @@ public void dbosCloudEnvOverridesConfigAppVerAndExecutor() throws Exception { .withAppVersion("test-app-version") .withExecutorId("test-executor-id"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); @@ -129,7 +129,7 @@ public void localExecutorId() throws Exception { .withDbUser("postgres") .withDbPassword(System.getenv("PGPASSWORD")); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); @@ -147,7 +147,7 @@ public void conductorExecutorId() throws Exception { .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_sys") .withConductorKey("test-conductor-key"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); @@ -167,7 +167,7 @@ public void cantSetExecutorIdWhenUsingConductor() throws Exception { .withConductorKey("test-conductor-key") .withExecutorId("test-executor-id"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { assertThrows(IllegalArgumentException.class, () -> DBOS.launch()); } finally { @@ -202,7 +202,7 @@ public void calcAppVersion() throws Exception { .withDbUser("postgres") .withDbPassword(System.getenv(Constants.POSTGRES_PASSWORD_ENV_VAR)); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); try { DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); @@ -240,7 +240,7 @@ public void configPGSimpleDataSource() throws Exception { .withDbUser("invalid-user") .withDbPassword("invalid-password"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); var impl = new HawkServiceImpl(); var proxy = DBOS.registerWorkflows(HawkService.class, impl); @@ -286,7 +286,7 @@ public void configHikariDataSource() throws Exception { .withDbUser("invalid-user") .withDbPassword("invalid-password"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); assertFalse(dataSource.isClosed()); var impl = new HawkServiceImpl(); @@ -334,7 +334,7 @@ public void appVersion() throws Exception { DBOSConfig.defaultsFromEnv("systemdbtest") .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_sys"); DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy = DBOS.registerWorkflows(ExecutorTestService.class, new ExecutorTestServiceImpl()); DBOS.launch(); diff --git a/transact/src/test/java/dev/dbos/transact/database/MetricsTest.java b/transact/src/test/java/dev/dbos/transact/database/MetricsTest.java index 94ba21af..1bc8847c 100644 --- a/transact/src/test/java/dev/dbos/transact/database/MetricsTest.java +++ b/transact/src/test/java/dev/dbos/transact/database/MetricsTest.java @@ -56,7 +56,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(config); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); proxy = DBOS.registerWorkflows(MetricsService.class, new MetricsServiceImpl()); DBOS.launch(); } diff --git a/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java b/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java index c075b539..28415b73 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java @@ -8,7 +8,6 @@ import dev.dbos.transact.context.WorkflowOptions; import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.exceptions.DBOSNonExistentWorkflowException; -import dev.dbos.transact.exceptions.DBOSWorkflowFunctionNotFoundException; import dev.dbos.transact.json.SerializationUtil; import dev.dbos.transact.utils.DBUtils; import dev.dbos.transact.workflow.*; @@ -44,7 +43,7 @@ void setUp() throws SQLException { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach @@ -152,41 +151,41 @@ void executeWorkflowByIdNonExistent() throws Exception { assertTrue(error); } - @Test - void workflowFunctionNotfound() throws Exception { + // @Test + // void workflowFunctionNotfound() throws Exception { - ExecutingService executingService = - DBOS.registerWorkflows(ExecutingService.class, new ExecutingServiceImpl()); - DBOS.launch(); + // ExecutingService executingService = + // DBOS.registerWorkflows(ExecutingService.class, new ExecutingServiceImpl()); + // DBOS.launch(); - String result = null; + // String result = null; - String wfid = "wf-123"; - try (var id = new WorkflowOptions(wfid).setContext()) { - result = executingService.workflowMethod("test-item"); - } + // String wfid = "wf-123"; + // try (var id = new WorkflowOptions(wfid).setContext()) { + // result = executingService.workflowMethod("test-item"); + // } - assertEquals("test-itemtest-item", result); + // assertEquals("test-itemtest-item", result); - List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); - assertEquals(wfs.get(0).status(), WorkflowState.SUCCESS.name()); + // List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); + // assertEquals(wfs.get(0).status(), WorkflowState.SUCCESS.name()); - DBOS.shutdown(); - DBOSTestAccess.clearRegistry(); // clear out the registry - DBOS.launch(); // restart dbos - var dbosExecutor = DBOSTestAccess.getDbosExecutor(); + // DBOS.shutdown(); + // DBOSTestAccess.clearRegistry(); // clear out the registry + // DBOS.launch(); // restart dbos + // var dbosExecutor = DBOSTestAccess.getDbosExecutor(); - boolean error = false; - try { - dbosExecutor.executeWorkflowById(wfid, false, false); - } catch (Exception e) { - error = true; - assert e instanceof DBOSWorkflowFunctionNotFoundException - : "Expected WorkflowFunctionNotfoundException but got " + e.getClass().getName(); - } + // boolean error = false; + // try { + // dbosExecutor.executeWorkflowById(wfid, false, false); + // } catch (Exception e) { + // error = true; + // assert e instanceof DBOSWorkflowFunctionNotFoundException + // : "Expected WorkflowFunctionNotfoundException but got " + e.getClass().getName(); + // } - assertTrue(error); - } + // assertTrue(error); + // } @Test public void executeWithStep() throws Exception { diff --git a/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java b/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java index b59d5073..4918f586 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java @@ -128,7 +128,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); impl = new LifecycleTestWorkflowsImpl(); DBOS.registerWorkflows(LifecycleTestWorkflows.class, impl, "inst1"); diff --git a/transact/src/test/java/dev/dbos/transact/execution/RecoveryServiceTest.java b/transact/src/test/java/dev/dbos/transact/execution/RecoveryServiceTest.java index 3787533f..a2ff2e1b 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/RecoveryServiceTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/RecoveryServiceTest.java @@ -53,7 +53,7 @@ void setUp() throws SQLException { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); executingService = DBOS.registerWorkflows( ExecutingService.class, executingServiceImpl = new ExecutingServiceImpl()); @@ -166,7 +166,7 @@ public void recoveryThreadTest() throws Exception { DBOS.shutdown(); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); // dbos = DBOS.getInstance(); // need to register again diff --git a/transact/src/test/java/dev/dbos/transact/execution/ScaleTest.java b/transact/src/test/java/dev/dbos/transact/execution/ScaleTest.java index 6e48853b..79e3a0ec 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/ScaleTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/ScaleTest.java @@ -55,7 +55,7 @@ public static void onetimeBefore() { @BeforeEach void setUp() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/execution/SingleExecutionTest.java b/transact/src/test/java/dev/dbos/transact/execution/SingleExecutionTest.java index 323f7cea..fbbe985d 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/SingleExecutionTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/SingleExecutionTest.java @@ -280,7 +280,7 @@ static void onetimeShutdown() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); execImpl = new TryConcExec(); execIfc = DBOS.registerWorkflows(TryConcExecIfc.class, execImpl); diff --git a/transact/src/test/java/dev/dbos/transact/invocation/CustomSchemaTest.java b/transact/src/test/java/dev/dbos/transact/invocation/CustomSchemaTest.java index f044ec71..01b1419b 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/CustomSchemaTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/CustomSchemaTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.utils.DBUtils; @@ -41,7 +42,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var impl = new HawkServiceImpl(); proxy = DBOS.registerWorkflows(HawkService.class, impl); impl.setProxy(proxy); diff --git a/transact/src/test/java/dev/dbos/transact/invocation/DirectInvocationTest.java b/transact/src/test/java/dev/dbos/transact/invocation/DirectInvocationTest.java index 70f0078e..7d6bd191 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/DirectInvocationTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/DirectInvocationTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; import dev.dbos.transact.database.SystemDatabase; @@ -45,7 +46,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var impl = new HawkServiceImpl(); proxy = DBOS.registerWorkflows(HawkService.class, impl); impl.setProxy(proxy); diff --git a/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java b/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java index 0e639587..f7ed34c5 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java @@ -44,7 +44,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); himpl = new HawkServiceImpl(); bimpla = new BearServiceImpl(); bimpl1 = new BearServiceImpl(); diff --git a/transact/src/test/java/dev/dbos/transact/invocation/PatchTest.java b/transact/src/test/java/dev/dbos/transact/invocation/PatchTest.java index ec8c6c50..dabd470d 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/PatchTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/PatchTest.java @@ -125,7 +125,7 @@ public void testPatch() throws Exception { try (var dataSource = SystemDatabase.createDataSource(dbosConfig)) { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy1 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplOne()); DBOS.launch(); @@ -140,7 +140,7 @@ public void testPatch() throws Exception { // Recreate DBOS with a new (patched) version of a workflow DBOS.shutdown(); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy2 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplTwo()); DBOS.launch(); @@ -165,7 +165,7 @@ public void testPatch() throws Exception { // Recreate DBOS with another new (patched) version of a workflow DBOS.shutdown(); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy3 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplThree()); DBOS.launch(); @@ -197,7 +197,7 @@ public void testPatch() throws Exception { // Now, let's deprecate the patch DBOS.shutdown(); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy4 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplFour()); DBOS.launch(); @@ -228,7 +228,7 @@ public void testPatch() throws Exception { // Now, let's deprecate the patch DBOS.shutdown(); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy5 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplFive()); DBOS.launch(); @@ -264,7 +264,7 @@ public void patchThrowsNotConfigured() throws Exception { .withAppVersion("test-version"); DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy2 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplTwo()); DBOS.launch(); @@ -280,7 +280,7 @@ public void deprecatePatchThrowsNotConfigured() throws Exception { .withAppVersion("test-version"); DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var proxy4 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplFour()); DBOS.launch(); @@ -296,7 +296,7 @@ public void mulipleDefinitions() throws Exception { .withAppVersion("test-version"); DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); @SuppressWarnings("unused") var proxy5 = DBOS.registerWorkflows(PatchService.class, new PatchServiceImplFive()); diff --git a/transact/src/test/java/dev/dbos/transact/invocation/StartWorkflowTest.java b/transact/src/test/java/dev/dbos/transact/invocation/StartWorkflowTest.java index 930acb44..6c79ec92 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/StartWorkflowTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/StartWorkflowTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.utils.DBUtils; @@ -39,7 +40,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); var impl = new HawkServiceImpl(); proxy = DBOS.registerWorkflows(HawkService.class, impl); impl.setProxy(proxy); diff --git a/transact/src/test/java/dev/dbos/transact/issues/Issue218.java b/transact/src/test/java/dev/dbos/transact/issues/Issue218.java index 72db377f..86b3a555 100644 --- a/transact/src/test/java/dev/dbos/transact/issues/Issue218.java +++ b/transact/src/test/java/dev/dbos/transact/issues/Issue218.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.database.SystemDatabase; @@ -92,7 +93,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); } diff --git a/transact/src/test/java/dev/dbos/transact/json/InteropTest.java b/transact/src/test/java/dev/dbos/transact/json/InteropTest.java index 5c12ce57..cad83203 100644 --- a/transact/src/test/java/dev/dbos/transact/json/InteropTest.java +++ b/transact/src/test/java/dev/dbos/transact/json/InteropTest.java @@ -4,6 +4,7 @@ import dev.dbos.transact.DBOS; import dev.dbos.transact.DBOSClient; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.utils.DBUtils; @@ -110,7 +111,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws Exception { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/json/PortableSerializationTest.java b/transact/src/test/java/dev/dbos/transact/json/PortableSerializationTest.java index c0c1bd1d..cec13a17 100644 --- a/transact/src/test/java/dev/dbos/transact/json/PortableSerializationTest.java +++ b/transact/src/test/java/dev/dbos/transact/json/PortableSerializationTest.java @@ -4,6 +4,7 @@ import dev.dbos.transact.DBOS; import dev.dbos.transact.DBOSClient; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.database.SystemDatabase; @@ -56,7 +57,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach @@ -798,7 +799,7 @@ public void testCustomSerializer() throws Exception { // Reinitialize with custom serializer var customConfig = dbosConfig.withSerializer(new TestBase64Serializer()); - DBOS.reinitialize(customConfig); + DBOSTestAccess.reinitialize(customConfig); Queue testQueue = new Queue("testq"); DBOS.registerQueue(testQueue); @@ -871,7 +872,7 @@ public void testCustomSerializerInterop() throws Exception { // Phase 2: Relaunch with custom serializer var customConfig = dbosConfig.withSerializer(new TestBase64Serializer()); - DBOS.reinitialize(customConfig); + DBOSTestAccess.reinitialize(customConfig); DBOS.registerQueue(testQueue); DBOS.registerWorkflows(EventSetterService.class, new EventSetterServiceImpl()); DBOS.launch(); @@ -901,7 +902,7 @@ public void testCustomSerializerInterop() throws Exception { DBOS.shutdown(); // Phase 3: Relaunch with custom serializer again, verify Phase 2 data still readable - DBOS.reinitialize(customConfig); + DBOSTestAccess.reinitialize(customConfig); DBOS.registerQueue(testQueue); DBOS.registerWorkflows(EventSetterService.class, new EventSetterServiceImpl()); DBOS.launch(); @@ -923,7 +924,7 @@ public void testCustomSerializerRemoved() throws Exception { // Launch with custom serializer var customConfig = dbosConfig.withSerializer(new TestBase64Serializer()); - DBOS.reinitialize(customConfig); + DBOSTestAccess.reinitialize(customConfig); Queue testQueue = new Queue("testq"); DBOS.registerQueue(testQueue); @@ -947,7 +948,7 @@ public void testCustomSerializerRemoved() throws Exception { DBOS.shutdown(); // Relaunch WITHOUT custom serializer - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); DBOS.registerQueue(testQueue); DBOS.registerWorkflows(EventSetterService.class, new EventSetterServiceImpl()); DBOS.launch(); diff --git a/transact/src/test/java/dev/dbos/transact/notifications/EventsTest.java b/transact/src/test/java/dev/dbos/transact/notifications/EventsTest.java index 1acc24f3..e0743932 100644 --- a/transact/src/test/java/dev/dbos/transact/notifications/EventsTest.java +++ b/transact/src/test/java/dev/dbos/transact/notifications/EventsTest.java @@ -179,7 +179,7 @@ static void oneTimeShutdown() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); impl = new EventsServiceImpl(); proxy = DBOS.registerWorkflows(EventsService.class, impl); DBOS.launch(); diff --git a/transact/src/test/java/dev/dbos/transact/notifications/NotificationServiceTest.java b/transact/src/test/java/dev/dbos/transact/notifications/NotificationServiceTest.java index 74c241d8..ef00f5d1 100644 --- a/transact/src/test/java/dev/dbos/transact/notifications/NotificationServiceTest.java +++ b/transact/src/test/java/dev/dbos/transact/notifications/NotificationServiceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; @@ -152,7 +153,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/queue/PartitionedQueuesTest.java b/transact/src/test/java/dev/dbos/transact/queue/PartitionedQueuesTest.java index b2029de5..0e19af77 100644 --- a/transact/src/test/java/dev/dbos/transact/queue/PartitionedQueuesTest.java +++ b/transact/src/test/java/dev/dbos/transact/queue/PartitionedQueuesTest.java @@ -7,6 +7,7 @@ import dev.dbos.transact.DBOS; import dev.dbos.transact.DBOSClient; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.database.SystemDatabase; @@ -97,7 +98,7 @@ void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/queue/QueuesTest.java b/transact/src/test/java/dev/dbos/transact/queue/QueuesTest.java index 080568eb..309c2262 100644 --- a/transact/src/test/java/dev/dbos/transact/queue/QueuesTest.java +++ b/transact/src/test/java/dev/dbos/transact/queue/QueuesTest.java @@ -52,7 +52,7 @@ void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach @@ -657,7 +657,7 @@ public void testQueueConcurrencyUnderRecovery() throws Exception { @Test public void testListenQueue() throws Exception { var config = dbosConfig.withListenQueue("queueOne"); - DBOS.reinitialize(config); + DBOSTestAccess.reinitialize(config); Queue queueOne = new Queue("queueOne"); Queue queueTwo = new Queue("queueTwo"); diff --git a/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java b/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java index 0577c9fa..68809223 100644 --- a/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java +++ b/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java @@ -33,7 +33,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/step/StepsTest.java b/transact/src/test/java/dev/dbos/transact/step/StepsTest.java index dd772514..347de35a 100644 --- a/transact/src/test/java/dev/dbos/transact/step/StepsTest.java +++ b/transact/src/test/java/dev/dbos/transact/step/StepsTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; import dev.dbos.transact.utils.DBUtils; @@ -30,7 +31,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java b/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java index 6ac9df2d..eb45713b 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; @@ -33,7 +34,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/workflow/ForkTest.java b/transact/src/test/java/dev/dbos/transact/workflow/ForkTest.java index 6235b555..a25d198d 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/ForkTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/ForkTest.java @@ -188,7 +188,7 @@ void beforeEachTest() throws SQLException { dataSource = SystemDatabase.createDataSource(dbosConfig); DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); impl = new ForkTestServiceImpl(); proxy = DBOS.registerWorkflows(ForkTestService.class, impl); diff --git a/transact/src/test/java/dev/dbos/transact/workflow/GarbageCollectionTest.java b/transact/src/test/java/dev/dbos/transact/workflow/GarbageCollectionTest.java index 0da5056a..cdaca90c 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/GarbageCollectionTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/GarbageCollectionTest.java @@ -73,7 +73,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); impl = new GCTestServiceImpl(); proxy = DBOS.registerWorkflows(GCTestService.class, impl); diff --git a/transact/src/test/java/dev/dbos/transact/workflow/ListWorkflowsTest.java b/transact/src/test/java/dev/dbos/transact/workflow/ListWorkflowsTest.java index a9696957..61af0f88 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/ListWorkflowsTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/ListWorkflowsTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.utils.DBUtils; @@ -57,7 +58,7 @@ static void onetimeSetup() throws Exception { DBOSConfig.defaultsFromEnv("systemdbtest") .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_sys"); DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); DBOS.launch(); baseTime = System.currentTimeMillis(); populateWorkflowsStatic(); diff --git a/transact/src/test/java/dev/dbos/transact/workflow/QueueChildWorkflowTest.java b/transact/src/test/java/dev/dbos/transact/workflow/QueueChildWorkflowTest.java index 8dfa1b57..88dcc212 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/QueueChildWorkflowTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/QueueChildWorkflowTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.utils.DBUtils; @@ -32,7 +33,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java b/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java index d4e7a57b..dc267257 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; import dev.dbos.transact.utils.DBUtils; @@ -30,7 +31,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/workflow/TimeoutTest.java b/transact/src/test/java/dev/dbos/transact/workflow/TimeoutTest.java index 9963a446..24fcfea6 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/TimeoutTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/TimeoutTest.java @@ -45,7 +45,7 @@ void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); dataSource = SystemDatabase.createDataSource(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/workflow/UnifiedProxyTest.java b/transact/src/test/java/dev/dbos/transact/workflow/UnifiedProxyTest.java index 7a254564..a7d5c6f4 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/UnifiedProxyTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/UnifiedProxyTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; @@ -33,7 +34,7 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); } @AfterEach diff --git a/transact/src/test/java/dev/dbos/transact/workflow/WorkflowMgmtTest.java b/transact/src/test/java/dev/dbos/transact/workflow/WorkflowMgmtTest.java index 790e365f..300382e7 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/WorkflowMgmtTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/WorkflowMgmtTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSTestAccess; import dev.dbos.transact.StartWorkflowOptions; import dev.dbos.transact.config.DBOSConfig; import dev.dbos.transact.context.WorkflowOptions; @@ -91,7 +92,7 @@ static void onetimeSetup() throws Exception { @BeforeEach void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - DBOS.reinitialize(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); impl = new MgmtServiceImpl(); proxy = DBOS.registerWorkflows(MgmtService.class, impl); From 349a5ba6bb397653c3b60af03ca2427100e87f0d Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 14:03:43 -0800 Subject: [PATCH 04/16] include internal queue in QueueRegistry.getSnapshot --- .../src/main/java/dev/dbos/transact/DBOS.java | 13 ++--- .../dbos/transact/database/WorkflowDAO.java | 2 +- .../dbos/transact/internal/QueueRegistry.java | 5 +- .../transact/execution/DBOSExecutorTest.java | 56 +++++++++---------- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index bb199374..1d84f816 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -254,13 +254,6 @@ public void registerAlertHandler(AlertHandler handler) { return dbosExecutor.get(); } - // // package private method for test purposes - // void clearRegistry() { - // workflowRegistry.clear(); - // queueRegistry.clear(); - // lifecycleRegistry.clear(); - // } - /** * Launch DBOS, and start recovery. All workflows, queues, and other objects should be * registered before launch @@ -811,8 +804,10 @@ public static void configure(DBOSConfig config) { * private method for test purposes */ static void reinitialize(DBOSConfig config) { - var instance = new DBOS.Instance(config); - globalInstance.set(instance); + var previousInstnace = globalInstance.getAndSet(new DBOS.Instance(config)); + if (previousInstnace != null) { + previousInstnace.shutdown(); + } } private static Instance ensureInstance() { diff --git a/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java b/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java index 517de711..03095bec 100644 --- a/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java +++ b/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java @@ -897,7 +897,7 @@ private static void insertForkedWorkflowStatus( stmt.setString(6, applicationVersion); stmt.setString(7, originalStatus.appId()); stmt.setString(8, originalStatus.authenticatedUser()); - stmt.setString(9, JSONUtil.serializeArray(originalStatus.authenticatedRoles())); + stmt.setString(9, originalStatus.authenticatedRoles() == null ? null : JSONUtil.serializeArray(originalStatus.authenticatedRoles())); stmt.setString(10, originalStatus.assumedRole()); stmt.setString(11, Constants.DBOS_INTERNAL_QUEUE); stmt.setString( diff --git a/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java b/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java index 7e3281de..8e8a3023 100644 --- a/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java +++ b/transact/src/main/java/dev/dbos/transact/internal/QueueRegistry.java @@ -3,6 +3,7 @@ import dev.dbos.transact.Constants; import dev.dbos.transact.workflow.Queue; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -50,6 +51,8 @@ public void clear() { } public List getSnapshot() { - return List.copyOf(registry.values()); + var queues = new ArrayList<>(registry.values()); + queues.add(internalQueue); + return List.copyOf(queues); } } diff --git a/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java b/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java index 28415b73..1bcc1813 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java @@ -8,6 +8,7 @@ import dev.dbos.transact.context.WorkflowOptions; import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.exceptions.DBOSNonExistentWorkflowException; +import dev.dbos.transact.exceptions.DBOSWorkflowFunctionNotFoundException; import dev.dbos.transact.json.SerializationUtil; import dev.dbos.transact.utils.DBUtils; import dev.dbos.transact.workflow.*; @@ -151,41 +152,40 @@ void executeWorkflowByIdNonExistent() throws Exception { assertTrue(error); } - // @Test - // void workflowFunctionNotfound() throws Exception { - - // ExecutingService executingService = - // DBOS.registerWorkflows(ExecutingService.class, new ExecutingServiceImpl()); - // DBOS.launch(); + @Test + void workflowFunctionNotfound() throws Exception { + ExecutingService executingService = + DBOS.registerWorkflows(ExecutingService.class, new ExecutingServiceImpl()); + DBOS.launch(); - // String result = null; + String result = null; - // String wfid = "wf-123"; - // try (var id = new WorkflowOptions(wfid).setContext()) { - // result = executingService.workflowMethod("test-item"); - // } + String wfid = "wf-123"; + try (var id = new WorkflowOptions(wfid).setContext()) { + result = executingService.workflowMethod("test-item"); + } - // assertEquals("test-itemtest-item", result); + assertEquals("test-itemtest-item", result); - // List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); - // assertEquals(wfs.get(0).status(), WorkflowState.SUCCESS.name()); + List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); + assertEquals(wfs.get(0).status(), WorkflowState.SUCCESS.name()); - // DBOS.shutdown(); - // DBOSTestAccess.clearRegistry(); // clear out the registry - // DBOS.launch(); // restart dbos - // var dbosExecutor = DBOSTestAccess.getDbosExecutor(); + DBOS.shutdown(); + DBOSTestAccess.reinitialize(dbosConfig); // reinitialize to clear out the registry + DBOS.launch(); + var dbosExecutor = DBOSTestAccess.getDbosExecutor(); - // boolean error = false; - // try { - // dbosExecutor.executeWorkflowById(wfid, false, false); - // } catch (Exception e) { - // error = true; - // assert e instanceof DBOSWorkflowFunctionNotFoundException - // : "Expected WorkflowFunctionNotfoundException but got " + e.getClass().getName(); - // } + boolean error = false; + try { + dbosExecutor.executeWorkflowById(wfid, false, false); + } catch (Exception e) { + error = true; + assert e instanceof DBOSWorkflowFunctionNotFoundException + : "Expected WorkflowFunctionNotfoundException but got " + e.getClass().getName(); + } - // assertTrue(error); - // } + assertTrue(error); + } @Test public void executeWithStep() throws Exception { From 76cc75387cfbcea68275427fa49d572e781b06f7 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 14:06:27 -0800 Subject: [PATCH 05/16] spotless --- .../main/java/dev/dbos/transact/database/WorkflowDAO.java | 6 +++++- .../java/dev/dbos/transact/execution/DBOSExecutorTest.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java b/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java index 03095bec..8eb43620 100644 --- a/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java +++ b/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java @@ -897,7 +897,11 @@ private static void insertForkedWorkflowStatus( stmt.setString(6, applicationVersion); stmt.setString(7, originalStatus.appId()); stmt.setString(8, originalStatus.authenticatedUser()); - stmt.setString(9, originalStatus.authenticatedRoles() == null ? null : JSONUtil.serializeArray(originalStatus.authenticatedRoles())); + stmt.setString( + 9, + originalStatus.authenticatedRoles() == null + ? null + : JSONUtil.serializeArray(originalStatus.authenticatedRoles())); stmt.setString(10, originalStatus.assumedRole()); stmt.setString(11, Constants.DBOS_INTERNAL_QUEUE); stmt.setString( diff --git a/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java b/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java index 1bcc1813..a2c5a12e 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/DBOSExecutorTest.java @@ -172,7 +172,7 @@ void workflowFunctionNotfound() throws Exception { DBOS.shutdown(); DBOSTestAccess.reinitialize(dbosConfig); // reinitialize to clear out the registry - DBOS.launch(); + DBOS.launch(); var dbosExecutor = DBOSTestAccess.getDbosExecutor(); boolean error = false; From 109eba04580552f479d67c4bb5eac552e41e0835 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 15:03:12 -0800 Subject: [PATCH 06/16] update dbos internals to use dbos.instance --- .../java/dev/dbos/transact/DBOSClient.java | 80 +++++++++++++------ .../dbos/transact/execution/DBOSExecutor.java | 40 +++++----- .../execution/DBOSLifecycleListener.java | 4 +- .../transact/execution/SchedulerService.java | 15 ++-- .../internal/WorkflowHandleDBPoll.java | 12 +-- .../internal/WorkflowHandleFuture.java | 11 ++- .../transact/execution/LifecycleTest.java | 10 ++- .../scheduled/SchedulerServiceTest.java | 4 +- 8 files changed, 110 insertions(+), 66 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOSClient.java b/transact/src/main/java/dev/dbos/transact/DBOSClient.java index 83e78a50..ae5a902c 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOSClient.java +++ b/transact/src/main/java/dev/dbos/transact/DBOSClient.java @@ -29,6 +29,33 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +class ClientWorkflowHandle implements WorkflowHandle { + + private final SystemDatabase systemDatabase; + private final String workflowId; + + public ClientWorkflowHandle(SystemDatabase systemDatabase, String workflowId) { + this.systemDatabase = systemDatabase; + this.workflowId = workflowId; + } + + @Override + public String workflowId() { + return workflowId; + } + + @Override + public T getResult() throws E { + var result = systemDatabase.awaitWorkflowResult(workflowId); + return Result.process(result); + } + + @Override + public WorkflowStatus getStatus() { + return systemDatabase.getWorkflowStatus(workflowId); + } +} + /** * DBOSClient allows external programs to interact with DBOS apps via direct system database access. * Example interactions: Start/enqueue a workflow, and get the result Get events and send messages @@ -470,31 +497,36 @@ public EnqueueOptions( String serializationFormat = options.serialization() != null ? options.serialization().formatName() : null; - return DBOSExecutor.enqueueWorkflow( - Objects.requireNonNull( - options.workflowName(), "EnqueueOptions workflowName must not be null"), - Objects.requireNonNull(options.className(), "EnqueueOptions className must not be null"), - Objects.requireNonNullElse(options.instanceName(), ""), - null, - args, - new DBOSExecutor.ExecutionOptions( - Objects.requireNonNullElseGet(options.workflowId(), () -> UUID.randomUUID().toString()), - Timeout.of(options.timeout()), - options.deadline, + var workflowId = + DBOSExecutor.enqueueWorkflow( + Objects.requireNonNull( + options.workflowName(), "EnqueueOptions workflowName must not be null"), Objects.requireNonNull( - options.queueName(), "EnqueueOptions queueName must not be null"), - options.deduplicationId, - options.priority, - options.queuePartitionKey, - false, - false, - serializationFormat), - null, - null, - null, - options.appVersion, - systemDatabase, - this.serializer); + options.className(), "EnqueueOptions className must not be null"), + Objects.requireNonNullElse(options.instanceName(), ""), + null, + args, + new DBOSExecutor.ExecutionOptions( + Objects.requireNonNullElseGet( + options.workflowId(), () -> UUID.randomUUID().toString()), + Timeout.of(options.timeout()), + options.deadline, + Objects.requireNonNull( + options.queueName(), "EnqueueOptions queueName must not be null"), + options.deduplicationId, + options.priority, + options.queuePartitionKey, + false, + false, + serializationFormat), + null, + null, + null, + options.appVersion, + systemDatabase, + this.serializer); + + return new ClientWorkflowHandle<>(systemDatabase, workflowId); } /** diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java index 1be74f93..ce3277db 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java @@ -195,7 +195,7 @@ public ExecutorService get() { listeners.add(schedulerService); for (var listener : listeners) { - listener.dbosLaunched(); + listener.dbosLaunched(dbos); } var recoveryTask = @@ -649,7 +649,7 @@ public T runStepInternal( /** Retrieve the workflowHandle for the workflowId */ public WorkflowHandle retrieveWorkflow(String workflowId) { logger.debug("retrieveWorkflow {}", workflowId); - return new WorkflowHandleDBPoll(workflowId); + return new WorkflowHandleDBPoll<>(this, workflowId); } public void sleep(Duration duration) { @@ -1277,19 +1277,21 @@ private WorkflowHandle executeWorkflow( "queue %s does not exist".formatted(options.queueName())); } - return enqueueWorkflow( - workflow.name(), - workflow.className(), - workflow.instanceName(), - maxRetries, - args, - options, - parent, - executorId(), - appVersion(), - appId(), - systemDatabase, - this.serializer); + var workflowId = + enqueueWorkflow( + workflow.name(), + workflow.className(), + workflow.instanceName(), + maxRetries, + args, + options, + parent, + executorId(), + appVersion(), + appId(), + systemDatabase, + this.serializer); + return new WorkflowHandleDBPoll<>(this, workflowId); } logger.debug("executeWorkflow {}({}) {}", workflow.fullyQualifiedName(), args, options); @@ -1412,10 +1414,10 @@ private WorkflowHandle executeWorkflow( TimeUnit.MILLISECONDS); } - return new WorkflowHandleFuture(workflowId, future, this); + return new WorkflowHandleFuture(this, workflowId, future); } - public static WorkflowHandle enqueueWorkflow( + public static String enqueueWorkflow( String name, String className, String instanceName, @@ -1467,10 +1469,10 @@ public static WorkflowHandle enqueueWorkflow( options.isDequeuedRequest, options.serialization(), serializer); - return new WorkflowHandleDBPoll(workflowId); + return workflowId; } catch (DBOSWorkflowExecutionConflictException e) { logger.debug("Workflow execution conflict for workflowId {}", workflowId); - return new WorkflowHandleDBPoll(workflowId); + return workflowId; } catch (Throwable e) { var actual = (e instanceof InvocationTargetException ite) ? ite.getTargetException() : e; logger.error("enqueueWorkflow", actual); diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSLifecycleListener.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSLifecycleListener.java index 31de26b8..27b48b87 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSLifecycleListener.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSLifecycleListener.java @@ -1,12 +1,14 @@ package dev.dbos.transact.execution; +import dev.dbos.transact.DBOS; + /** * For registering callbacks that hear about `DBOS.launch()` and `DBOS.shutdown()`. At this point, * DBOS is ready to run workflows, and no additional registrations are allowed. */ public interface DBOSLifecycleListener { /** Called from within DBOS.launch, after workflow processing is allowed */ - void dbosLaunched(); + void dbosLaunched(DBOS.Instance dbos); /** Called from within DBOS.shutdown, before workflow processing is stopped */ void dbosShutDown(); diff --git a/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java b/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java index 462628e4..53f27eb6 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java +++ b/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java @@ -35,6 +35,7 @@ public class SchedulerService implements DBOSLifecycleListener { new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING53)); private final String schedulerQueueName; + private DBOS.Instance dbos; private final AtomicReference scheduler = new AtomicReference<>(); public SchedulerService(String defSchedulerQueue) { @@ -59,7 +60,8 @@ public static void validateScheduledWorkflow(RegisteredWorkflow workflow) { cronParser.parse(skedTag.cron()); } - public void dbosLaunched() { + public void dbosLaunched(DBOS.Instance dbos) { + this.dbos = dbos; if (this.scheduler.get() == null) { var scheduler = Executors.newScheduledThreadPool(4); if (this.scheduler.compareAndSet(null, scheduler)) { @@ -69,6 +71,7 @@ public void dbosLaunched() { } public void dbosShutDown() { + this.dbos = null; var scheduler = this.scheduler.getAndSet(null); if (scheduler != null) { List notRun = scheduler.shutdownNow(); @@ -82,7 +85,7 @@ record ScheduledWorkflow( private ZonedDateTime getLastTime(ScheduledWorkflow swf) { if (!swf.ignoreMissed()) { var state = - DBOS.getExternalState( + dbos.getExternalState( "DBOS.SchedulerService", swf.workflow().fullyQualifiedName(), "lastTime"); if (state.isPresent()) { return ZonedDateTime.parse(state.get().value()); @@ -97,7 +100,7 @@ private ZonedDateTime setLastTime(ScheduledWorkflow swf, ZonedDateTime lastTime) } var state = - DBOS.upsertExternalState( + dbos.upsertExternalState( new ExternalState( "DBOS.SchedulerService", swf.workflow().fullyQualifiedName(), @@ -115,7 +118,7 @@ private void startScheduledWorkflows() { // collect all workflows that have an @Scheduled annotation List scheduledWorkflows = new ArrayList<>(); - for (var wf : DBOS.getRegisteredWorkflows()) { + for (var wf : dbos.getRegisteredWorkflows()) { var method = wf.workflowMethod(); var skedTag = method.getAnnotation(Scheduled.class); if (skedTag == null) { @@ -134,7 +137,7 @@ private void startScheduledWorkflows() { skedTag.queue() != null && !skedTag.queue().isEmpty() ? skedTag.queue() : this.schedulerQueueName; - var q = DBOS.getQueue(queue); + var q = dbos.getQueue(queue); if (!q.isPresent()) { logger.error( "Scheduled workflow {} refers to undefined queue {}", wf.fullyQualifiedName(), queue); @@ -202,7 +205,7 @@ public void run() { String workflowId = String.format("sched-%s-%s", workflowName, scheduledTime.toString()); var options = new StartWorkflowOptions(workflowId).withQueue(swf.queue()); - DBOS.startWorkflow(swf.workflow(), args, options); + dbos.startWorkflow(swf.workflow(), args, options); nextTime = setLastTime(swf, scheduledTime); } catch (Exception e) { logger.error("Scheduled task exception {}", workflowName, e); diff --git a/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleDBPoll.java b/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleDBPoll.java index 776f6cc3..d7bd263a 100644 --- a/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleDBPoll.java +++ b/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleDBPoll.java @@ -1,13 +1,15 @@ package dev.dbos.transact.workflow.internal; -import dev.dbos.transact.DBOS; +import dev.dbos.transact.execution.DBOSExecutor; import dev.dbos.transact.workflow.WorkflowHandle; import dev.dbos.transact.workflow.WorkflowStatus; public class WorkflowHandleDBPoll implements WorkflowHandle { - private String workflowId; + private final DBOSExecutor executor; + private final String workflowId; - public WorkflowHandleDBPoll(String workflowId) { + public WorkflowHandleDBPoll(DBOSExecutor executor, String workflowId) { + this.executor = executor; this.workflowId = workflowId; } @@ -18,11 +20,11 @@ public String workflowId() { @Override public T getResult() throws E { - return DBOS.getResult(this.workflowId); + return executor.getResult(this.workflowId); } @Override public WorkflowStatus getStatus() { - return DBOS.getWorkflowStatus(workflowId); + return executor.getWorkflowStatus(workflowId); } } diff --git a/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleFuture.java b/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleFuture.java index 7de7b4dc..dc8c9dc7 100644 --- a/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleFuture.java +++ b/transact/src/main/java/dev/dbos/transact/workflow/internal/WorkflowHandleFuture.java @@ -1,6 +1,5 @@ package dev.dbos.transact.workflow.internal; -import dev.dbos.transact.DBOS; import dev.dbos.transact.exceptions.DBOSAwaitedWorkflowCancelledException; import dev.dbos.transact.exceptions.DBOSWorkflowExecutionConflictException; import dev.dbos.transact.execution.DBOSExecutor; @@ -13,11 +12,11 @@ public class WorkflowHandleFuture implements WorkflowHandle { - private String workflowId; - private Future futureResult; - private DBOSExecutor executor; + private final DBOSExecutor executor; + private final String workflowId; + private final Future futureResult; - public WorkflowHandleFuture(String workflowId, Future future, DBOSExecutor executor) { + public WorkflowHandleFuture(DBOSExecutor executor, String workflowId, Future future) { this.workflowId = workflowId; this.futureResult = future; this.executor = executor; @@ -58,6 +57,6 @@ public T getResult() throws E { @Override public WorkflowStatus getStatus() { - return DBOS.getWorkflowStatus(workflowId); + return executor.getWorkflowStatus(workflowId); } } diff --git a/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java b/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java index 4918f586..d2a98a45 100644 --- a/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java +++ b/transact/src/test/java/dev/dbos/transact/execution/LifecycleTest.java @@ -61,6 +61,7 @@ public int doNotRunWF(int nInstances, int nWfs) { } class TestLifecycleService implements DBOSLifecycleListener { + private DBOS.Instance dbos; public int launchCount = 0; public int shutdownCount = 0; public int nInstances = 0; @@ -70,13 +71,14 @@ class TestLifecycleService implements DBOSLifecycleListener { public ArrayList wfs = new ArrayList<>(); @Override - public void dbosLaunched() { + public void dbosLaunched(DBOS.Instance dbos) { + this.dbos = dbos; var expectedParams = new Class[] {int.class, int.class}; ++launchCount; - nInstances = DBOS.getRegisteredWorkflowInstances().size(); - var wfs = DBOS.getRegisteredWorkflows(); + nInstances = dbos.getRegisteredWorkflowInstances().size(); + var wfs = dbos.getRegisteredWorkflows(); for (var wf : wfs) { var method = wf.workflowMethod(); var tag = method.getAnnotation(TestLifecycleAnnotation.class); @@ -105,7 +107,7 @@ public int runThemAll() throws Exception { int total = 0; for (var wf : wfs) { Object[] args = {nInstances, nWfs}; - var h = DBOS.startWorkflow(wf, args, new StartWorkflowOptions(UUID.randomUUID().toString())); + var h = dbos.startWorkflow(wf, args, new StartWorkflowOptions(UUID.randomUUID().toString())); total += (Integer) h.getResult(); } return total; diff --git a/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java b/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java index 68809223..22575a96 100644 --- a/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java +++ b/transact/src/test/java/dev/dbos/transact/scheduled/SchedulerServiceTest.java @@ -97,10 +97,12 @@ public void simpleScheduledWorkflow() throws Exception { assertTrue(q2workflows.size() >= 1); assertEquals("q2", q2workflows.get(0).queueName()); + DBOS.shutdown(); + // See about makeup work (ignore missed) var timeToSleep = 5000 - (System.currentTimeMillis() - timeAsOfShutdown); Thread.sleep(timeToSleep < 0 ? 0 : timeToSleep); - schedulerService.dbosLaunched(); + DBOS.launch(); Thread.sleep(2000); int count1imb = impl.everySecondCounterIgnoreMissed; From 0c280d760500b930f895dcf260f8b36128bbcf90 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 15:14:03 -0800 Subject: [PATCH 07/16] add Instance Test --- .../transact/invocation/InstanceTest.java | 511 ++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java diff --git a/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java b/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java new file mode 100644 index 00000000..9bbb3941 --- /dev/null +++ b/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java @@ -0,0 +1,511 @@ +package dev.dbos.transact.invocation; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.dbos.transact.DBOS; +import dev.dbos.transact.config.DBOSConfig; +import dev.dbos.transact.context.WorkflowOptions; +import dev.dbos.transact.database.SystemDatabase; +import dev.dbos.transact.exceptions.DBOSAwaitedWorkflowCancelledException; +import dev.dbos.transact.utils.DBUtils; +import dev.dbos.transact.workflow.Step; +import dev.dbos.transact.workflow.Timeout; +import dev.dbos.transact.workflow.Workflow; + +import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HawkServiceInstanceImpl implements HawkService { + private final DBOS.Instance dbos; + private HawkService proxy; + + public HawkServiceInstanceImpl(DBOS.Instance dbos) { + this.dbos = dbos; + } + + public void setProxy(HawkService proxy) { + this.proxy = proxy; + } + + @Workflow + @Override + public String simpleWorkflow() { + return LocalDate.now().format(DateTimeFormatter.ISO_DATE); + } + + @Workflow + @Override + public String sleepWorkflow(long sleepSec) { + var duration = Duration.ofSeconds(sleepSec); + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return LocalDate.now().format(DateTimeFormatter.ISO_DATE); + } + + @Workflow + @Override + public String parentWorkflow() { + return proxy.simpleWorkflow(); + } + + @Workflow + @Override + public String parentStartWorkflow() { + var handle = dbos.startWorkflow(() -> proxy.simpleWorkflow()); + return handle.getResult(); + } + + @Workflow + @Override + public String parentSleepWorkflow(Long timeoutSec, long sleepSec) { + var duration = + timeoutSec == null + ? Timeout.inherit() + : timeoutSec == 0L ? Timeout.none() : Timeout.of(Duration.ofSeconds(timeoutSec)); + var options = new WorkflowOptions().withTimeout(duration); + try (var o = options.setContext()) { + return proxy.sleepWorkflow(sleepSec); + } + } + + @Step + @Override + public Instant nowStep() { + return Instant.now(); + } + + @Workflow + @Override + public Instant stepWorkflow() { + return proxy.nowStep(); + } + + @Step + @Override + public String illegalStep() { + return proxy.simpleWorkflow(); + } + + @Workflow + @Override + public String illegalWorkflow() { + return proxy.illegalStep(); + } +} + +@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) +public class InstanceTest { + private static DBOSConfig dbosConfig; + private DBOS.Instance dbos; + private HawkService proxy; + private HikariDataSource dataSource; + private String localDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE); + + @BeforeAll + static void onetimeSetup() throws Exception { + + dbosConfig = + DBOSConfig.defaultsFromEnv("systemdbtest") + .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_sys"); + } + + @BeforeEach + void beforeEachTest() throws SQLException { + DBUtils.recreateDB(dbosConfig); + + // Note, manually injecting the DBOS instance here is a poor developer experience + // Opened https://github.com/dbos-inc/dbos-transact-java/issues/296 to track improving this + + dbos = new DBOS.Instance(dbosConfig); + var impl = new HawkServiceInstanceImpl(dbos); + proxy = dbos.registerWorkflows(HawkService.class, impl); + impl.setProxy(proxy); + + dbos.launch(); + + dataSource = SystemDatabase.createDataSource(dbosConfig); + } + + @AfterEach + void afterEachTest() throws Exception { + dataSource.close(); + dbos.shutdown(); + } + + @Test + void directInvoke() throws Exception { + + var result = proxy.simpleWorkflow(); + assertEquals(localDate, result); + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertDoesNotThrow(() -> UUID.fromString((String) row.workflowId())); + assertEquals("SUCCESS", row.status()); + assertEquals("simpleWorkflow", row.name()); + assertEquals("dev.dbos.transact.invocation.HawkServiceInstanceImpl", row.className()); + assertNotNull(row.output()); + assertNull(row.error()); + assertNull(row.timeoutMs()); + assertNull(row.deadlineEpochMs()); + } + + @Test + void directInvokeSetWorkflowId() throws Exception { + + String workflowId = "directInvokeSetWorkflowId"; + try (var _o = new WorkflowOptions(workflowId).setContext()) { + var result = proxy.simpleWorkflow(); + assertEquals(localDate, result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertEquals(workflowId, row.workflowId()); + } + + @Test + void directInvokeSetTimeout() throws Exception { + + var options = new WorkflowOptions().withTimeout(Duration.ofSeconds(10)); + try (var _o = options.setContext()) { + var result = proxy.sleepWorkflow(1); + assertEquals(localDate, result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertEquals(10000L, row.timeoutMs()); + assertNotNull(row.deadlineEpochMs()); + } + + @Test + void directInvokeSetZeroTimeout() throws Exception { + + var options = new WorkflowOptions().withTimeout(Timeout.none()); + try (var _o = options.setContext()) { + var result = proxy.sleepWorkflow(1); + assertEquals(localDate, result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertNull(row.timeoutMs()); + assertNull(row.deadlineEpochMs()); + } + + @Test + void directInvokeSetWorkflowIdAndTimeout() throws Exception { + + String workflowId = "directInvokeSetWorkflowIdAndTimeout"; + var options = new WorkflowOptions(workflowId).withTimeout(Duration.ofSeconds(10)); + try (var _o = options.setContext()) { + var result = proxy.sleepWorkflow(1); + assertEquals(localDate, result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertEquals(workflowId, row.workflowId()); + assertEquals(10000L, row.timeoutMs()); + assertNotNull(row.deadlineEpochMs()); + } + + @Test + void directInvokeTimeoutCancellation() throws Exception { + + var options = new WorkflowOptions().withTimeout(Duration.ofSeconds(1)); + try (var _o = options.setContext()) { + assertThrows(DBOSAwaitedWorkflowCancelledException.class, () -> proxy.sleepWorkflow(10L)); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertEquals("CANCELLED", row.status()); + assertNull(row.output()); + assertNull(row.error()); + } + + @Test + void directInvokeTimeoutDeadline() throws Exception { + + var options = + new WorkflowOptions().withDeadline(Instant.ofEpochMilli(System.currentTimeMillis() + 1000)); + try (var _o = options.setContext()) { + assertThrows(DBOSAwaitedWorkflowCancelledException.class, () -> proxy.sleepWorkflow(10L)); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertEquals("CANCELLED", row.status()); + assertNull(row.output()); + assertNull(row.error()); + } + + @Test + void directInvokeSetWorkflowIdTimeoutCancellation() throws Exception { + + var workflowId = "directInvokeSetWorkflowIdTimeoutCancellation"; + var options = new WorkflowOptions(workflowId).withTimeout(Duration.ofSeconds(1)); + try (var _o = options.setContext()) { + assertThrows(DBOSAwaitedWorkflowCancelledException.class, () -> proxy.sleepWorkflow(10L)); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, rows.size()); + var row = rows.get(0); + assertEquals(workflowId, row.workflowId()); + assertEquals("CANCELLED", row.status()); + assertNull(row.output()); + assertNull(row.error()); + } + + @Test + void directInvokeParent() throws Exception { + + var result = proxy.parentWorkflow(); + assertEquals(localDate, result); + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + assertDoesNotThrow(() -> UUID.fromString(row0.workflowId())); + assertEquals(row0.workflowId() + "-0", row1.workflowId()); + assertEquals("SUCCESS", row0.status()); + assertEquals("SUCCESS", row1.status()); + assertEquals("parentWorkflow", row0.name()); + assertEquals("simpleWorkflow", row1.name()); + assertEquals(row0.output(), row1.output()); + assertNull(row0.timeoutMs()); + assertNull(row1.timeoutMs()); + assertNull(row0.deadlineEpochMs()); + assertNull(row1.deadlineEpochMs()); + assertNull(row0.parentWorkflowId()); + assertEquals(row0.workflowId(), row1.parentWorkflowId()); + + var steps = DBUtils.getStepRows(dataSource, row0.workflowId()); + assertEquals(1, steps.size()); + var step = steps.get(0); + assertEquals(row0.workflowId(), step.workflowId()); + assertEquals(0, step.functionId()); + assertNull(step.output()); + assertNull(step.error()); + assertEquals("simpleWorkflow", step.functionName()); + assertEquals(row1.workflowId(), step.childWorkflowId()); + } + + @Test + void directInvokeParentStartWorkflow() throws Exception { + var result = proxy.parentStartWorkflow(); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + assertDoesNotThrow(() -> UUID.fromString(row0.workflowId())); + assertEquals(row0.workflowId() + "-0", row1.workflowId()); + assertEquals("SUCCESS", row0.status()); + assertEquals("SUCCESS", row1.status()); + assertEquals("parentStartWorkflow", row0.name()); + assertEquals("simpleWorkflow", row1.name()); + assertEquals(row0.output(), row1.output()); + assertNull(row0.timeoutMs()); + assertNull(row1.timeoutMs()); + assertNull(row0.deadlineEpochMs()); + assertNull(row1.deadlineEpochMs()); + + var steps = DBUtils.getStepRows(dataSource, row0.workflowId()); + assertEquals(2, steps.size()); + var step = steps.get(0); + var gr = steps.get(1); + assertEquals(row0.workflowId(), step.workflowId()); + assertEquals(0, step.functionId()); + assertNull(step.output()); + assertNull(step.error()); + assertEquals("simpleWorkflow", step.functionName()); + assertEquals(row1.workflowId(), step.childWorkflowId()); + assertEquals("DBOS.getResult", gr.functionName()); + } + + @Test + void directInvokeParentSetWorkflowId() throws Exception { + + String workflowId = "directInvokeParentSetWorkflowId"; + try (var _o = new WorkflowOptions(workflowId).setContext()) { + var result = proxy.parentWorkflow(); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + assertEquals(workflowId, row0.workflowId()); + assertEquals(workflowId + "-0", row1.workflowId()); + assertEquals(workflowId, row1.parentWorkflowId()); + } + + @Test + void directInvokeParentSetTimeout() throws Exception { + + var options = new WorkflowOptions().withTimeout(Duration.ofSeconds(10)); + try (var _o = options.setContext()) { + var result = proxy.parentSleepWorkflow(null, 1); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + assertEquals(10000L, row0.timeoutMs()); + assertEquals(10000L, row1.timeoutMs()); + assertNotNull(row0.deadlineEpochMs()); + assertNotNull(row1.deadlineEpochMs()); + assertEquals(row0.deadlineEpochMs(), row1.deadlineEpochMs()); + } + + @Test + void directInvokeParentSetTimeoutParent() throws Exception { + + var result = proxy.parentSleepWorkflow(5L, 1); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + assertNull(row0.timeoutMs()); + assertNull(row0.deadlineEpochMs()); + assertEquals(5000L, row1.timeoutMs()); + assertNotNull(row1.deadlineEpochMs()); + } + + @Test + void directInvokeParentSetTimeoutParent2() throws Exception { + + var options = new WorkflowOptions().withTimeout(Duration.ofSeconds(10)); + try (var _o = options.setContext()) { + var result = proxy.parentSleepWorkflow(5L, 1); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + + assertEquals(10000L, row0.timeoutMs()); + assertNotNull(row0.deadlineEpochMs()); + + assertEquals(5000L, row1.timeoutMs()); + assertNotNull(row1.deadlineEpochMs()); + } + + @Test + void directInvokeParentSetTimeoutParent3() throws Exception { + + var options = new WorkflowOptions().withTimeout(Duration.ofSeconds(10)); + try (var _o = options.setContext()) { + var result = proxy.parentSleepWorkflow(0L, 1); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + } + + var rows = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, rows.size()); + var row0 = rows.get(0); + var row1 = rows.get(1); + + assertEquals(10000L, row0.timeoutMs()); + assertNotNull(row0.deadlineEpochMs()); + + assertNull(row1.timeoutMs()); + assertNull(row1.deadlineEpochMs()); + } + + @Test + void invokeWorkflowFromStepThrows() throws Exception { + var ise = assertThrows(IllegalStateException.class, () -> proxy.illegalWorkflow()); + assertEquals("cannot invoke a workflow from a step", ise.getMessage()); + + var wfs = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, wfs.size()); + var wf = wfs.get(0); + assertNotNull(wf.workflowId()); + + var steps = dbos.listWorkflowSteps(wf.workflowId()); + assertEquals(1, steps.size()); + var step = steps.get(0); + assertEquals(0, step.functionId()); + assertNull(step.output()); + assertEquals("cannot invoke a workflow from a step", step.error().message()); + assertEquals("cannot invoke a workflow from a step", step.error().throwable().getMessage()); + assertEquals("illegalStep", step.functionName()); + } + + @Test + void directInvokeStep() throws Exception { + var result = proxy.stepWorkflow(); + assertNotNull(result); + + var wfs = DBUtils.getWorkflowRows(dataSource); + assertEquals(1, wfs.size()); + var wf = wfs.get(0); + assertNotNull(wf.workflowId()); + + var steps = DBUtils.getStepRows(dataSource, wf.workflowId()); + assertEquals(1, steps.size()); + var step = steps.get(0); + assertEquals(0, step.functionId()); + assertNotNull(step.output()); + assertNull(step.error()); + assertEquals("nowStep", step.functionName()); + } + + @Test + void directInvokeParentSetParentTimeout() throws Exception { + + var options = new WorkflowOptions().withTimeout(Duration.ofSeconds(10)); + try (var _o = options.setContext()) { + var result = proxy.parentWorkflow(); + assertEquals(LocalDate.now().format(DateTimeFormatter.ISO_DATE), result); + } + + var table = DBUtils.getWorkflowRows(dataSource); + assertEquals(2, table.size()); + var row0 = table.get(0); + var row1 = table.get(1); + assertEquals(10000L, row0.timeoutMs()); + assertEquals(10000L, row1.timeoutMs()); + assertNotNull(row0.deadlineEpochMs()); + assertNotNull(row1.deadlineEpochMs()); + assertEquals(row0.deadlineEpochMs(), row1.deadlineEpochMs()); + } +} From d25333ff1c34a0ad7e6ce2baf5cbe09517c22068 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 15:59:02 -0800 Subject: [PATCH 08/16] copilot feedback --- transact/src/main/java/dev/dbos/transact/DBOS.java | 11 +++++------ .../dev/dbos/transact/execution/DBOSExecutor.java | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index 1d84f816..cab91904 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -259,8 +259,7 @@ public void registerAlertHandler(AlertHandler handler) { * registered before launch */ public void launch() { - var ver = DBOS.version(); - logger.info("Launching DBOS {}", ver == null ? "" : "v" + ver); + logger.info("Launching DBOS v{}", DBOS.version()); if (dbosExecutor.get() == null) { var executor = new DBOSExecutor(config); @@ -804,9 +803,9 @@ public static void configure(DBOSConfig config) { * private method for test purposes */ static void reinitialize(DBOSConfig config) { - var previousInstnace = globalInstance.getAndSet(new DBOS.Instance(config)); - if (previousInstnace != null) { - previousInstnace.shutdown(); + var previousInstance = globalInstance.getAndSet(new DBOS.Instance(config)); + if (previousInstance != null) { + previousInstance.shutdown(); } } @@ -1115,7 +1114,7 @@ public static void send( * @param timeout duration after which the call times out * @return the message if there is one or else null */ - public static @Nullable Object recv(@NonNull String topic, @NonNull Duration timeout) { + public static @Nullable Object recv(@Nullable String topic, @NonNull Duration timeout) { return ensureInstance().recv(topic, timeout); } diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java index ce3277db..34000cc8 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java @@ -1417,7 +1417,7 @@ private WorkflowHandle executeWorkflow( return new WorkflowHandleFuture(this, workflowId, future); } - public static String enqueueWorkflow( + public static String enqueueWorkflow( String name, String className, String instanceName, From c45b162266e24fad4d08f5ba9f265a03e3b64524 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 10:29:26 -0800 Subject: [PATCH 09/16] handle missing defaults in send, sendDirect and forkWorkflow --- .../main/java/dev/dbos/transact/database/NotificationsDAO.java | 3 +++ .../src/main/java/dev/dbos/transact/database/WorkflowDAO.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java b/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java index df12f5e9..572eced6 100644 --- a/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java +++ b/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java @@ -5,6 +5,7 @@ import dev.dbos.transact.exceptions.DBOSWorkflowExecutionConflictException; import dev.dbos.transact.json.DBOSSerializer; import dev.dbos.transact.json.SerializationUtil; +import dev.dbos.transact.workflow.SerializationStrategy; import dev.dbos.transact.workflow.internal.StepResult; import java.sql.Connection; @@ -53,6 +54,8 @@ void send( String serialization) throws SQLException { + serialization = + Objects.requireNonNullElse(serialization, SerializationStrategy.DEFAULT.formatName()); var startTime = System.currentTimeMillis(); String functionName = "DBOS.send"; String finalTopic = (topic != null) ? topic : Constants.DBOS_NULL_TOPIC; diff --git a/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java b/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java index 8eb43620..35e572f8 100644 --- a/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java +++ b/transact/src/main/java/dev/dbos/transact/database/WorkflowDAO.java @@ -812,7 +812,7 @@ void resumeWorkflow(String workflowId) throws SQLException { String forkWorkflow(String originalWorkflowId, int startStep, ForkOptions options) throws SQLException { - Objects.requireNonNull(options); + options = Objects.requireNonNullElseGet(options, ForkOptions::new); var status = getWorkflowStatus(originalWorkflowId); if (status == null) { From 0522ee9ef9a9cc7b6c49b5239b621bfb845b4776 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 11:03:23 -0800 Subject: [PATCH 10/16] remove incorrect serialization default --- .../main/java/dev/dbos/transact/database/NotificationsDAO.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java b/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java index 572eced6..df12f5e9 100644 --- a/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java +++ b/transact/src/main/java/dev/dbos/transact/database/NotificationsDAO.java @@ -5,7 +5,6 @@ import dev.dbos.transact.exceptions.DBOSWorkflowExecutionConflictException; import dev.dbos.transact.json.DBOSSerializer; import dev.dbos.transact.json.SerializationUtil; -import dev.dbos.transact.workflow.SerializationStrategy; import dev.dbos.transact.workflow.internal.StepResult; import java.sql.Connection; @@ -54,8 +53,6 @@ void send( String serialization) throws SQLException { - serialization = - Objects.requireNonNullElse(serialization, SerializationStrategy.DEFAULT.formatName()); var startTime = System.currentTimeMillis(); String functionName = "DBOS.send"; String finalTopic = (topic != null) ? topic : Constants.DBOS_NULL_TOPIC; From 7e804888f23b44b1aa053cd3fad5e1e63adf239a Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Wed, 4 Mar 2026 17:17:56 -0800 Subject: [PATCH 11/16] flow DBOSExecutor thru DBOSContext --- .../src/main/java/dev/dbos/transact/DBOS.java | 368 ++++++++++-------- .../dbos/transact/context/DBOSContext.java | 25 +- .../dbos/transact/execution/DBOSExecutor.java | 1 + .../transact/execution/ThrowingRunnable.java | 7 + .../dev/dbos/transact/DBOSExtensions.kt | 2 + .../transact/invocation/InstanceTest.java | 91 +---- 6 files changed, 231 insertions(+), 263 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index cab91904..e823811d 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -910,6 +910,56 @@ public static void shutdown() { ensureInstance().shutdown(); } + /** + * Get all workflows registered with DBOS. + * + * @return list of all registered workflow methods + */ + public static @NonNull Collection getRegisteredWorkflows() { + // TODO: no step wrapping in executor + return ensureInstance().getRegisteredWorkflows(); + } + + /** + * Get all workflow classes registered with DBOS. + * + * @return list of all class instances containing registered workflow methods + */ + public static @NonNull Collection getRegisteredWorkflowInstances() { + // TODO: no step wrapping in executor + return ensureInstance().getRegisteredWorkflowInstances(); + } + + /** + * Retrieve a queue definition + * + * @param queueName Name of the queue + * @return Queue definition for given `queueName` + */ + public static @NonNull Optional getQueue(@NonNull String queueName) { + // TODO: no step wrapping in executor + var executor = DBOSContext.dbosExecutor(); + return executor != null ? executor.getQueue(queueName) : ensureInstance().getQueue(queueName); + } + + /** + * Retrieve a handle to a workflow, given its ID. Note that a handle is always returned, whether + * the workflow exists or not; getStatus() can be used to tell the difference + * + * @param Return type of the workflow function + * @param Checked exception thrown by the workflow function, if any + * @param workflowId ID of the workflow to retrieve + * @return Workflow handle for the provided workflow ID + */ + public static @NonNull WorkflowHandle retrieveWorkflow( + @NonNull String workflowId) { + // TODO: no step wrapping in executor + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.retrieveWorkflow(workflowId) + : ensureInstance().retrieveWorkflow(workflowId); + } + /** * Get the ID of the current running workflow, or `null` if a workflow is not in progress * @@ -953,36 +1003,15 @@ public static boolean inStep() { } /** - * Retrieve a queue definition - * - * @param queueName Name of the queue - * @return Queue definition for given `queueName` - */ - public static @NonNull Optional getQueue(@NonNull String queueName) { - return ensureInstance().getQueue(queueName); - } - - /** - * Durable sleep. Use this instead of Thread.sleep, especially in workflows. On restart or during - * recovery the original expected wakeup time is honoured as opposed to sleeping all over again. - * - * @param duration amount of time to sleep - */ - public static void sleep(@NonNull Duration duration) { - ensureInstance().sleep(duration); - } - - /** - * Start or enqueue a workflow with default options + * Start or enqueue a workflow with no return value, using default options * - * @param Return type of the workflow * @param Type of checked exception thrown by the workflow, if any - * @param supplier A lambda that calls exactly one workflow function + * @param runnable A lambda that calls exactly one workflow function * @return A handle to the enqueued or running workflow */ - public static @NonNull WorkflowHandle startWorkflow( - @NonNull ThrowingSupplier supplier) { - return ensureInstance().startWorkflow(supplier, new StartWorkflowOptions()); + public static @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingRunnable runnable) { + return DBOS.startWorkflow(runnable, new StartWorkflowOptions()); } /** @@ -995,19 +1024,20 @@ public static void sleep(@NonNull Duration duration) { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingRunnable runnable, @NonNull StartWorkflowOptions options) { - return ensureInstance().startWorkflow(runnable, options); + return DBOS.startWorkflow(runnable.asSupplier(), options); } /** - * Start or enqueue a workflow with no return value, using default options + * Start or enqueue a workflow with default options * + * @param Return type of the workflow * @param Type of checked exception thrown by the workflow, if any - * @param runnable A lambda that calls exactly one workflow function + * @param supplier A lambda that calls exactly one workflow function * @return A handle to the enqueued or running workflow */ - public static @NonNull WorkflowHandle startWorkflow( - @NonNull ThrowingRunnable runnable) { - return ensureInstance().startWorkflow(runnable, new StartWorkflowOptions()); + public static @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingSupplier supplier) { + return DBOS.startWorkflow(supplier, new StartWorkflowOptions()); } /** @@ -1021,44 +1051,65 @@ public static void sleep(@NonNull Duration duration) { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingSupplier supplier, @NonNull StartWorkflowOptions options) { - return ensureInstance().startWorkflow(supplier, options); + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.startWorkflow(supplier, options) + : ensureInstance().startWorkflow(supplier, options); } /** - * Execute a workflow based on registration and arguments. This is expected to be used by event - * listeners, not app code. + * Run the provided function as a step; this variant is for functions with no return value * - * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows - * @param args Workflow function arguments - * @param options Execution options, such as ID, queue, and timeout/deadline - * @return WorkflowHandle to the executed workflow + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param name Name of the step, for tracing and recording in the system database + * @throws E */ - public static WorkflowHandle startWorkflow( - RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { - return ensureInstance().startWorkflow(regWorkflow, args, options); + public static void runStep( + @NonNull ThrowingRunnable stepfunc, @NonNull String name) throws E { + DBOS.runStep(stepfunc, new StepOptions(name)); } /** - * Get the result of a workflow, or rethrow the exception thrown by the workflow + * Run the provided function as a step; this variant is for functions with no return value * - * @param Return type of the workflow - * @param Checked exception type, if any, thrown by the workflow - * @param workflowId ID of the workflow to retrieve - * @return Return value of the workflow - * @throws E if the workflow threw an exception + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E */ - public static T getResult(@NonNull String workflowId) throws E { - return ensureInstance().getResult(workflowId); + public static void runStep( + @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { + DBOS.runStep(stepfunc.asSupplier(), opts); } /** - * Get the status of a workflow + * Run the provided function as a step; this variant is for functions with a return value * - * @param workflowId ID of the workflow to query - * @return Current workflow status for the provided workflowId, or null. + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param name name of the step, for tracing and to record in the system database + * @throws E */ - public static @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { - return ensureInstance().getWorkflowStatus(workflowId); + public static T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { + return DBOS.runStep(stepfunc, new StepOptions(name)); + } + + /** + * Run the provided function as a step; this variant is for functions with a return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E + */ + public static T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.runStepInternal(stepfunc, opts, null) + : ensureInstance().runStep(stepfunc, opts); } /** @@ -1074,7 +1125,7 @@ public static void send( @NonNull Object message, @Nullable String topic, @Nullable String idempotencyKey) { - ensureInstance().send(destinationId, message, topic, idempotencyKey); + DBOS.send(destinationId, message, topic, idempotencyKey, null); } /** @@ -1086,7 +1137,7 @@ public static void send( */ public static void send( @NonNull String destinationId, @NonNull Object message, @NonNull String topic) { - ensureInstance().send(destinationId, message, topic); + DBOS.send(destinationId, message, topic, null, null); } /** @@ -1104,7 +1155,12 @@ public static void send( @Nullable String topic, @Nullable String idempotencyKey, @Nullable SerializationStrategy serialization) { - ensureInstance().send(destinationId, message, topic, idempotencyKey, serialization); + var executor = DBOSContext.dbosExecutor(); + if (executor != null) { + executor.send(destinationId, message, topic, idempotencyKey, serialization); + } else { + ensureInstance().send(destinationId, message, topic, idempotencyKey, serialization); + } } /** @@ -1115,7 +1171,8 @@ public static void send( * @return the message if there is one or else null */ public static @Nullable Object recv(@Nullable String topic, @NonNull Duration timeout) { - return ensureInstance().recv(topic, timeout); + var executor = DBOSContext.dbosExecutor(); + return executor != null ? executor.recv(topic, timeout) : ensureInstance().recv(topic, timeout); } /** @@ -1125,7 +1182,7 @@ public static void send( * @param value data that is published */ public static void setEvent(@NonNull String key, @NonNull Object value) { - ensureInstance().setEvent(key, value); + DBOS.setEvent(key, value, null); } /** @@ -1137,7 +1194,12 @@ public static void setEvent(@NonNull String key, @NonNull Object value) { */ public static void setEvent( @NonNull String key, @NonNull Object value, @Nullable SerializationStrategy serialization) { - ensureInstance().setEvent(key, value, serialization); + var executor = DBOSContext.dbosExecutor(); + if (executor != null) { + executor.setEvent(key, value, serialization); + } else { + ensureInstance().setEvent(key, value, serialization); + } } /** @@ -1150,61 +1212,39 @@ public static void setEvent( */ public static @Nullable Object getEvent( @NonNull String workflowId, @NonNull String key, @NonNull Duration timeout) { - return ensureInstance().getEvent(workflowId, key, timeout); - } - - /** - * Run the provided function as a step; this variant is for functions with a return value - * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param name name of the step, for tracing and to record in the system database - * @throws E - */ - public static T runStep( - @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { - - return ensureInstance().runStep(stepfunc, name); - } - - /** - * Run the provided function as a step; this variant is for functions with no return value - * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param opts step name, and retry options for running the step - * @throws E - */ - public static void runStep( - @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { - ensureInstance().runStep(stepfunc, opts); + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.getEvent(workflowId, key, timeout) + : ensureInstance().getEvent(workflowId, key, timeout); } /** - * Run the provided function as a step; this variant is for functions with no return value + * Get the result of a workflow, or rethrow the exception thrown by the workflow * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param name Name of the step, for tracing and recording in the system database - * @throws E + * @param Return type of the workflow + * @param Checked exception type, if any, thrown by the workflow + * @param workflowId ID of the workflow to retrieve + * @return Return value of the workflow + * @throws E if the workflow threw an exception */ - public static void runStep( - @NonNull ThrowingRunnable stepfunc, @NonNull String name) throws E { - ensureInstance().runStep(stepfunc, name); + public static T getResult(@NonNull String workflowId) throws E { + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.getResult(workflowId) + : ensureInstance().getResult(workflowId); } /** - * Run the provided function as a step; this variant is for functions with a return value + * Get the status of a workflow * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param opts step name, and retry options for running the step - * @throws E + * @param workflowId ID of the workflow to query + * @return Current workflow status for the provided workflowId, or null. */ - public static T runStep( - @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { - - return ensureInstance().runStep(stepfunc, opts); + public static @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.getWorkflowStatus(workflowId) + : ensureInstance().getWorkflowStatus(workflowId); } /** @@ -1217,7 +1257,10 @@ public static T runStep( */ public static @NonNull WorkflowHandle resumeWorkflow( @NonNull String workflowId) { - return ensureInstance().resumeWorkflow(workflowId); + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.resumeWorkflow(workflowId) + : ensureInstance().resumeWorkflow(workflowId); } /*** @@ -1228,7 +1271,12 @@ public static T runStep( * @param workflowId ID of the workflow to cancel */ public static void cancelWorkflow(@NonNull String workflowId) { - ensureInstance().cancelWorkflow(workflowId); + var executor = DBOSContext.dbosExecutor(); + if (executor != null) { + executor.cancelWorkflow(workflowId); + } else { + ensureInstance().cancelWorkflow(workflowId); + } } /** @@ -1239,12 +1287,11 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @param Checked exception thrown by the workflow function, if any * @param workflowId Original workflow Id * @param startStep Start execution from this step. Prior steps copied over - * @param options {@link ForkOptions} containing forkedWorkflowId, applicationVersion, timeout * @return handle to the workflow */ public static @NonNull WorkflowHandle forkWorkflow( - @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { - return ensureInstance().forkWorkflow(workflowId, startStep, options); + @NonNull String workflowId, int startStep) { + return DBOS.forkWorkflow(workflowId, startStep, null); } /** @@ -1255,11 +1302,15 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @param Checked exception thrown by the workflow function, if any * @param workflowId Original workflow Id * @param startStep Start execution from this step. Prior steps copied over + * @param options {@link ForkOptions} containing forkedWorkflowId, applicationVersion, timeout * @return handle to the workflow */ public static @NonNull WorkflowHandle forkWorkflow( - @NonNull String workflowId, int startStep) { - return ensureInstance().forkWorkflow(workflowId, startStep); + @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.forkWorkflow(workflowId, startStep, options) + : ensureInstance().forkWorkflow(workflowId, startStep, options); } /** @@ -1269,7 +1320,7 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public static void deleteWorkflow(@NonNull String workflowId) { - ensureInstance().deleteWorkflow(workflowId); + DBOS.deleteWorkflow(workflowId, false); } /** @@ -1281,21 +1332,12 @@ public static void deleteWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { - ensureInstance().deleteWorkflow(workflowId, deleteChildren); - } - - /** - * Retrieve a handle to a workflow, given its ID. Note that a handle is always returned, whether - * the workflow exists or not; getStatus() can be used to tell the difference - * - * @param Return type of the workflow function - * @param Checked exception thrown by the workflow function, if any - * @param workflowId ID of the workflow to retrieve - * @return Workflow handle for the provided workflow ID - */ - public static @NonNull WorkflowHandle retrieveWorkflow( - @NonNull String workflowId) { - return ensureInstance().retrieveWorkflow(workflowId); + var executor = DBOSContext.dbosExecutor(); + if (executor != null) { + executor.deleteWorkflow(workflowId, deleteChildren); + } else { + ensureInstance().deleteWorkflow(workflowId, deleteChildren); + } } /** @@ -1305,7 +1347,8 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return a list of workflow status {@link WorkflowStatus} */ public static @NonNull List listWorkflows(@NonNull ListWorkflowsInput input) { - return ensureInstance().listWorkflows(input); + var executor = DBOSContext.dbosExecutor(); + return executor != null ? executor.listWorkflows(input) : ensureInstance().listWorkflows(input); } /** @@ -1315,51 +1358,24 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return list of step information {@link StepInfo} */ public static @NonNull List listWorkflowSteps(@NonNull String workflowId) { - return ensureInstance().listWorkflowSteps(workflowId); - } - - /** - * Get all workflows registered with DBOS. - * - * @return list of all registered workflow methods - */ - public static @NonNull Collection getRegisteredWorkflows() { - return ensureInstance().getRegisteredWorkflows(); - } - - /** - * Get all workflow classes registered with DBOS. - * - * @return list of all class instances containing registered workflow methods - */ - public static @NonNull Collection getRegisteredWorkflowInstances() { - return ensureInstance().getRegisteredWorkflowInstances(); - } - - /** - * Get a system database record stored by an external service A unique value is stored per - * combination of service, workflowName, and key - * - * @param service Identity of the service maintaining the record - * @param workflowName Fully qualified name of the workflow - * @param key Key assigned within the service+workflow - * @return Value associated with the service+workflow+key combination - */ - public static Optional getExternalState( - String service, String workflowName, String key) { - return ensureInstance().getExternalState(service, workflowName, key); + var executor = DBOSContext.dbosExecutor(); + return executor != null + ? executor.listWorkflowSteps(workflowId) + : ensureInstance().listWorkflowSteps(workflowId); } /** - * Insert or update a system database record stored by an external service A timestamped unique - * value is stored per combination of service, workflowName, and key + * Durable sleep. Use this instead of Thread.sleep, especially in workflows. On restart or during + * recovery the original expected wakeup time is honoured as opposed to sleeping all over again. * - * @param state ExternalState containing the service, workflow, key, and value to store - * @return Value associated with the service+workflow+key combination, in case the stored value - * already had a higher version or timestamp + * @param duration amount of time to sleep */ - public static ExternalState upsertExternalState(ExternalState state) { - return ensureInstance().upsertExternalState(state); + public static void sleep(@NonNull Duration duration) { + var executor = DBOSContext.dbosExecutor(); + if (executor == null) { + throw new IllegalStateException("DBOS.sleep must be called from a workflow"); + } + executor.sleep(duration); } /** @@ -1377,7 +1393,11 @@ public static ExternalState upsertExternalState(ExternalState state) { * workflow */ public static boolean patch(@NonNull String patchName) { - return ensureInstance().patch(patchName); + var executor = DBOSContext.dbosExecutor(); + if (executor == null) { + throw new IllegalStateException("DBOS.patch must be called from a workflow"); + } + return executor.patch(patchName); } /** @@ -1393,6 +1413,10 @@ public static boolean patch(@NonNull String patchName) { * workflow */ public static boolean deprecatePatch(@NonNull String patchName) { - return ensureInstance().deprecatePatch(patchName); + var executor = DBOSContext.dbosExecutor(); + if (executor == null) { + throw new IllegalStateException("DBOS.deprecatePatch must be called from a workflow"); + } + return executor.deprecatePatch(patchName); } } diff --git a/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java b/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java index 499e69b1..b8e2e3dd 100644 --- a/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java +++ b/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java @@ -1,6 +1,7 @@ package dev.dbos.transact.context; import dev.dbos.transact.StartWorkflowOptions; +import dev.dbos.transact.execution.DBOSExecutor; import dev.dbos.transact.workflow.SerializationStrategy; import dev.dbos.transact.workflow.Timeout; @@ -10,6 +11,8 @@ public class DBOSContext { + DBOSExecutor dbosExecutor; + // assigned context options String nextWorkflowId; Timeout nextTimeout; @@ -27,6 +30,7 @@ public class DBOSContext { // private StepStatus stepStatus; public DBOSContext() { + dbosExecutor = null; workflowId = null; functionId = -1; parent = null; @@ -35,16 +39,23 @@ public DBOSContext() { serialization = SerializationStrategy.DEFAULT; } - public DBOSContext(String workflowId, WorkflowInfo parent, Duration timeout, Instant deadline) { - this(workflowId, parent, timeout, deadline, null); + public DBOSContext( + DBOSExecutor dbosExecutor, + String workflowId, + WorkflowInfo parent, + Duration timeout, + Instant deadline) { + this(dbosExecutor, workflowId, parent, timeout, deadline, null); } public DBOSContext( + DBOSExecutor dbosExecutor, String workflowId, WorkflowInfo parent, Duration timeout, Instant deadline, SerializationStrategy serialization) { + this.dbosExecutor = dbosExecutor; this.workflowId = workflowId; this.functionId = 0; this.parent = parent; @@ -58,6 +69,7 @@ public DBOSContext( StartWorkflowOptions options, Integer functionId, CompletableFuture future) { + this.dbosExecutor = other.dbosExecutor; this.nextWorkflowId = other.nextWorkflowId; this.nextTimeout = other.nextTimeout; this.nextDeadline = other.nextDeadline; @@ -144,6 +156,10 @@ public void setSerializationStrategy(SerializationStrategy strat) { this.serialization = strat; } + public DBOSExecutor getDbosExecutor() { + return dbosExecutor; + } + public static String workflowId() { var ctx = DBOSContextHolder.get(); return ctx == null ? null : ctx.workflowId; @@ -168,4 +184,9 @@ public static SerializationStrategy serializationStrategy() { var ctx = DBOSContextHolder.get(); return ctx != null ? ctx.getSerialization() : null; } + + public static DBOSExecutor dbosExecutor() { + var ctx = DBOSContextHolder.get(); + return ctx != null ? ctx.getDbosExecutor() : null; + } } diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java index 1aa78a94..15d45417 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java @@ -1346,6 +1346,7 @@ private WorkflowHandle executeWorkflow( DBOSContextHolder.set( new DBOSContext( + this, workflowId, parent, foptions.timeoutDuration(), diff --git a/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java b/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java index 6d382714..c41094a4 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java +++ b/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java @@ -3,4 +3,11 @@ @FunctionalInterface public interface ThrowingRunnable { void execute() throws E; + + default ThrowingSupplier asSupplier() { + return () -> { + this.execute(); + return null; + }; + } } diff --git a/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt b/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt index 5d174be1..5b072dc5 100644 --- a/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt +++ b/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt @@ -157,11 +157,13 @@ fun getRegisteredWorkflows() = DBOS.getRegisteredWorkflows() @JvmSynthetic fun getRegisteredWorkflowInstances() = DBOS.getRegisteredWorkflowInstances() +/** @JvmSynthetic fun getExternalState(service: String, workflowName: String, key: String) = DBOS.getExternalState(service, workflowName, key) @JvmSynthetic fun upsertExternalState(state: ExternalState) = DBOS.upsertExternalState(state) +*/ @JvmSynthetic fun patch(name: String) = DBOS.patch(name) diff --git a/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java b/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java index 9bbb3941..eb4bd5d6 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java @@ -12,9 +12,7 @@ import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.exceptions.DBOSAwaitedWorkflowCancelledException; import dev.dbos.transact.utils.DBUtils; -import dev.dbos.transact.workflow.Step; import dev.dbos.transact.workflow.Timeout; -import dev.dbos.transact.workflow.Workflow; import java.sql.SQLException; import java.time.Duration; @@ -29,88 +27,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class HawkServiceInstanceImpl implements HawkService { - private final DBOS.Instance dbos; - private HawkService proxy; - - public HawkServiceInstanceImpl(DBOS.Instance dbos) { - this.dbos = dbos; - } - - public void setProxy(HawkService proxy) { - this.proxy = proxy; - } - - @Workflow - @Override - public String simpleWorkflow() { - return LocalDate.now().format(DateTimeFormatter.ISO_DATE); - } - - @Workflow - @Override - public String sleepWorkflow(long sleepSec) { - var duration = Duration.ofSeconds(sleepSec); - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - return LocalDate.now().format(DateTimeFormatter.ISO_DATE); - } - - @Workflow - @Override - public String parentWorkflow() { - return proxy.simpleWorkflow(); - } - - @Workflow - @Override - public String parentStartWorkflow() { - var handle = dbos.startWorkflow(() -> proxy.simpleWorkflow()); - return handle.getResult(); - } - - @Workflow - @Override - public String parentSleepWorkflow(Long timeoutSec, long sleepSec) { - var duration = - timeoutSec == null - ? Timeout.inherit() - : timeoutSec == 0L ? Timeout.none() : Timeout.of(Duration.ofSeconds(timeoutSec)); - var options = new WorkflowOptions().withTimeout(duration); - try (var o = options.setContext()) { - return proxy.sleepWorkflow(sleepSec); - } - } - - @Step - @Override - public Instant nowStep() { - return Instant.now(); - } - - @Workflow - @Override - public Instant stepWorkflow() { - return proxy.nowStep(); - } - - @Step - @Override - public String illegalStep() { - return proxy.simpleWorkflow(); - } - - @Workflow - @Override - public String illegalWorkflow() { - return proxy.illegalStep(); - } -} - @org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) public class InstanceTest { private static DBOSConfig dbosConfig; @@ -131,11 +47,8 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); - // Note, manually injecting the DBOS instance here is a poor developer experience - // Opened https://github.com/dbos-inc/dbos-transact-java/issues/296 to track improving this - dbos = new DBOS.Instance(dbosConfig); - var impl = new HawkServiceInstanceImpl(dbos); + var impl = new HawkServiceImpl(); proxy = dbos.registerWorkflows(HawkService.class, impl); impl.setProxy(proxy); @@ -162,7 +75,7 @@ void directInvoke() throws Exception { assertDoesNotThrow(() -> UUID.fromString((String) row.workflowId())); assertEquals("SUCCESS", row.status()); assertEquals("simpleWorkflow", row.name()); - assertEquals("dev.dbos.transact.invocation.HawkServiceInstanceImpl", row.className()); + assertEquals("dev.dbos.transact.invocation.HawkServiceImpl", row.className()); assertNotNull(row.output()); assertNull(row.error()); assertNull(row.timeoutMs()); From 690cf6088e2a5d39359fbb1f54cd826befc5850a Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 14:40:07 -0800 Subject: [PATCH 12/16] verify executors match in start workflow --- .../dbos/transact/execution/DBOSExecutor.java | 4 + .../internal/DBOSInvocationHandler.java | 5 +- .../dbos/transact/internal/Invocation.java | 8 +- .../transact/invocation/MultiInstTest.java | 224 ------------------ .../java/dev/dbos/transact/utils/DBUtils.java | 6 + 5 files changed, 20 insertions(+), 227 deletions(-) delete mode 100644 transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java index 15d45417..bf832768 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java @@ -1089,6 +1089,10 @@ public WorkflowHandle startWorkflow( logger.debug("startWorkflow {}", options); var invocation = captureInvocation(supplier); + if (invocation.executor() != this) { + throw new IllegalStateException( + "The @Workflow method must be called on the DBOS instance passed to the startWorkflow lambda"); + } var workflow = getWorkflow(invocation); var ctx = DBOSContextHolder.get(); diff --git a/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java b/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java index 07ccbf4b..19894fd7 100644 --- a/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java +++ b/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java @@ -97,13 +97,14 @@ protected Object handleWorkflow( ? classNameAnnotation.value() : target.getClass().getName(); var workflowName = workflow.name().isEmpty() ? method.getName() : workflow.name(); + var executor = executorSupplier.get(); + if (hook != null) { - var invocation = new Invocation(className, instanceName, workflowName, args); + var invocation = new Invocation(executor, className, instanceName, workflowName, args); hook.invoke(invocation); return defaultReturn(method); } - var executor = executorSupplier.get(); if (executor == null) { throw new IllegalStateException("executorSupplier returned null"); } diff --git a/transact/src/main/java/dev/dbos/transact/internal/Invocation.java b/transact/src/main/java/dev/dbos/transact/internal/Invocation.java index d922184a..f6f1f426 100644 --- a/transact/src/main/java/dev/dbos/transact/internal/Invocation.java +++ b/transact/src/main/java/dev/dbos/transact/internal/Invocation.java @@ -1,4 +1,10 @@ package dev.dbos.transact.internal; +import dev.dbos.transact.execution.DBOSExecutor; + public record Invocation( - String className, String instanceName, String workflowName, Object[] args) {} + DBOSExecutor executor, + String className, + String instanceName, + String workflowName, + Object[] args) {} diff --git a/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java b/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java deleted file mode 100644 index f7ed34c5..00000000 --- a/transact/src/test/java/dev/dbos/transact/invocation/MultiInstTest.java +++ /dev/null @@ -1,224 +0,0 @@ -package dev.dbos.transact.invocation; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.dbos.transact.DBOS; -import dev.dbos.transact.DBOSClient; -import dev.dbos.transact.DBOSTestAccess; -import dev.dbos.transact.config.DBOSConfig; -import dev.dbos.transact.database.SystemDatabase; -import dev.dbos.transact.utils.DBUtils; -import dev.dbos.transact.workflow.ListWorkflowsInput; -import dev.dbos.transact.workflow.Queue; -import dev.dbos.transact.workflow.WorkflowState; - -import java.sql.SQLException; -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) -public class MultiInstTest { - private static DBOSConfig dbosConfig; - HawkServiceImpl himpl; - BearServiceImpl bimpla; - BearServiceImpl bimpl1; - private HawkService hproxy; - private BearService bproxya; - private BearService bproxy1; - private String localDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE); - - @BeforeAll - static void onetimeSetup() throws Exception { - dbosConfig = - DBOSConfig.defaultsFromEnv("systemdbtest") - .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_sys"); - } - - @BeforeEach - void beforeEachTest() throws SQLException { - DBUtils.recreateDB(dbosConfig); - DBOSTestAccess.reinitialize(dbosConfig); - himpl = new HawkServiceImpl(); - bimpla = new BearServiceImpl(); - bimpl1 = new BearServiceImpl(); - DBOS.registerQueue(new Queue("testQueue")); - - hproxy = DBOS.registerWorkflows(HawkService.class, himpl); - himpl.setProxy(hproxy); - - bproxya = DBOS.registerWorkflows(BearService.class, bimpla, "A"); - bimpla.setProxy(bproxya); - - bproxy1 = DBOS.registerWorkflows(BearService.class, bimpl1, "1"); - bimpl1.setProxy(bproxy1); - - DBOS.launch(); - } - - @AfterEach - void afterEachTest() throws Exception { - DBOS.shutdown(); - } - - @Test - void startWorkflow() throws Exception { - var bhandlea = - DBOS.startWorkflow( - () -> { - return bproxya.stepWorkflow(); - }); - var bresulta = bhandlea.getResult(); - assertEquals( - localDate, - bresulta - .atZone(ZoneId.systemDefault()) // or another zone - .toLocalDate() - .format(DateTimeFormatter.ISO_DATE)); - assertEquals(1, bimpla.nWfCalls); - - var bhandle1 = - DBOS.startWorkflow( - () -> { - return bproxy1.stepWorkflow(); - }); - var bresult1 = bhandle1.getResult(); - assertEquals( - localDate, - bresult1 - .atZone(ZoneId.systemDefault()) // or another zone - .toLocalDate() - .format(DateTimeFormatter.ISO_DATE)); - assertEquals(1, bimpl1.nWfCalls); - - var hhandle = - DBOS.startWorkflow( - () -> { - return hproxy.stepWorkflow(); - }); - var hresult = hhandle.getResult(); - assertEquals( - localDate, - hresult - .atZone(ZoneId.systemDefault()) // or another zone - .toLocalDate() - .format(DateTimeFormatter.ISO_DATE)); - - var browsa = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowId(bhandlea.workflowId())); - assertEquals(1, browsa.size()); - var browa = browsa.get(0); - assertEquals(bhandlea.workflowId(), browa.workflowId()); - assertEquals("stepWorkflow", browa.name()); - assertEquals("A", browa.instanceName()); - assertEquals("dev.dbos.transact.invocation.BearServiceImpl", browa.className()); - assertEquals("SUCCESS", browa.status()); - - var brows1 = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowId(bhandle1.workflowId())); - assertEquals(1, brows1.size()); - var brow1 = brows1.get(0); - assertEquals(bhandle1.workflowId(), brow1.workflowId()); - assertEquals("stepWorkflow", brow1.name()); - assertEquals("1", brow1.instanceName()); - assertEquals("dev.dbos.transact.invocation.BearServiceImpl", brow1.className()); - assertEquals("SUCCESS", brow1.status()); - - var hrows = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowId(hhandle.workflowId())); - assertEquals(1, hrows.size()); - var hrow = hrows.get(0); - assertEquals(hhandle.workflowId(), hrow.workflowId()); - assertEquals("stepWorkflow", hrow.name()); - assertEquals("dev.dbos.transact.invocation.HawkServiceImpl", hrow.className()); - assertEquals("", hrow.instanceName()); - assertEquals("SUCCESS", hrow.status()); - - // All 3 w/ the same WF name - var allrows = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowName("stepWorkflow")); - assertEquals(3, allrows.size()); - - // 2 from BSI - var brows = - DBOS.listWorkflows( - new ListWorkflowsInput() - .withWorkflowName("stepWorkflow") - .withClassName("dev.dbos.transact.invocation.BearServiceImpl")); - assertEquals(2, brows.size()); - - // 2 from BSI - var browsjust1 = - DBOS.listWorkflows( - new ListWorkflowsInput() - .withWorkflowName("stepWorkflow") - .withClassName("dev.dbos.transact.invocation.BearServiceImpl") - .withInstanceName("1")); - assertEquals(1, browsjust1.size()); - } - - private static final String dbUrl = "jdbc:postgresql://localhost:5432/dbos_java_sys"; - private static final String dbUser = "postgres"; - private static final String dbPassword = System.getenv("PGPASSWORD"); - - @Test - public void enqueueForSpecificInstance() throws Exception { - try (var client = new DBOSClient(dbUrl, dbUser, dbPassword)) { - var options = - new DBOSClient.EnqueueOptions( - "dev.dbos.transact.invocation.BearServiceImpl", "stepWorkflow", "testQueue") - .withInstanceName("A"); - var handle = client.enqueueWorkflow(options, new Object[] {}); - - var result = handle.getResult(); - assertEquals( - localDate, - result - .atZone(ZoneId.systemDefault()) // or another zone - .toLocalDate() - .format(DateTimeFormatter.ISO_DATE)); - - assertEquals(1, bimpla.nWfCalls); - assertEquals(0, bimpl1.nWfCalls); - - var stat = client.getWorkflowStatus(handle.workflowId()); - assertEquals( - "SUCCESS", - stat.orElseThrow(() -> new AssertionError("Workflow status not found")).status()); - - try (var dataSource = SystemDatabase.createDataSource(dbosConfig)) { - DBUtils.setWorkflowState(dataSource, handle.workflowId(), WorkflowState.PENDING.name()); - } - stat = client.getWorkflowStatus(handle.workflowId()); - assertEquals( - "PENDING", - stat.orElseThrow(() -> new AssertionError("Workflow status not found")).status()); - - var dbosExecutor = DBOSTestAccess.getDbosExecutor(); - var eh = dbosExecutor.executeWorkflowById(handle.workflowId(), false, true); - eh.getResult(); - stat = client.getWorkflowStatus(handle.workflowId()); - assertEquals( - "SUCCESS", - stat.orElseThrow(() -> new AssertionError("Workflow status not found")).status()); - assertEquals(0, bimpl1.nWfCalls); - assertEquals(2, bimpla.nWfCalls); - } - } - - @Test - void listSteps() throws Exception { - var bh = DBOS.startWorkflow(() -> bproxya.stepWorkflow()); - bh.getResult(); - var sh = DBOS.startWorkflow(() -> bproxya.listSteps(bh.workflowId())); - var ss = sh.getResult(); - assertEquals("1 1", ss); - - var steps = DBOS.listWorkflowSteps(sh.workflowId()); - assertEquals(2, steps.size()); - assertEquals("DBOS.listWorkflows", steps.get(0).functionName()); - assertEquals("DBOS.listWorkflowSteps", steps.get(1).functionName()); - } -} diff --git a/transact/src/test/java/dev/dbos/transact/utils/DBUtils.java b/transact/src/test/java/dev/dbos/transact/utils/DBUtils.java index 33aca87a..f527dc27 100644 --- a/transact/src/test/java/dev/dbos/transact/utils/DBUtils.java +++ b/transact/src/test/java/dev/dbos/transact/utils/DBUtils.java @@ -208,6 +208,12 @@ public static Connection getConnection(DBOSConfig config) throws SQLException { return DriverManager.getConnection(config.databaseUrl(), config.dbUser(), config.dbPassword()); } + public static List getWorkflowRows(DBOSConfig config) throws SQLException { + try (var ds = SystemDatabase.createDataSource(config)) { + return getWorkflowRows(ds); + } + } + public static List getWorkflowRows(DataSource ds) throws SQLException { return getWorkflowRows(ds, null); } From 3f9fe9b199cccf4e7ced2867da2525465e1247cd Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 14:40:28 -0800 Subject: [PATCH 13/16] add mock and multi dbos tests --- .../invocation/MockDbosInstanceTest.java | 71 ++++++ .../invocation/MultiClassInstanceTest.java | 224 ++++++++++++++++++ .../invocation/MultiDbosInstanceTest.java | 151 ++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 transact/src/test/java/dev/dbos/transact/invocation/MockDbosInstanceTest.java create mode 100644 transact/src/test/java/dev/dbos/transact/invocation/MultiClassInstanceTest.java create mode 100644 transact/src/test/java/dev/dbos/transact/invocation/MultiDbosInstanceTest.java diff --git a/transact/src/test/java/dev/dbos/transact/invocation/MockDbosInstanceTest.java b/transact/src/test/java/dev/dbos/transact/invocation/MockDbosInstanceTest.java new file mode 100644 index 00000000..21bb5ff1 --- /dev/null +++ b/transact/src/test/java/dev/dbos/transact/invocation/MockDbosInstanceTest.java @@ -0,0 +1,71 @@ +package dev.dbos.transact.invocation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.dbos.transact.DBOS; +import dev.dbos.transact.execution.ThrowingSupplier; +import dev.dbos.transact.workflow.Workflow; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; + +interface MockTestService { + String testWorkflow(); +} + +class MockTestServiceImpl implements MockTestService { + private final DBOS.Instance dbos; + + public MockTestServiceImpl(DBOS.Instance instance) { + this.dbos = instance; + } + + @Workflow + @Override + public String testWorkflow() { + var today = dbos.runStep(() -> LocalDate.now(), "todaysDate"); + dbos.setEvent("greetEvent", today); + var name = dbos.recv("greetTopic", Duration.ofSeconds(30)); + return String.format("Hello %s, today is %s", name, today.format(DateTimeFormatter.ISO_DATE)); + } +} + +@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) +public class MockDbosInstanceTest { + @Test + public void testMockInstance() throws Exception { + var mockDBOS = mock(DBOS.Instance.class); + var impl = new MockTestServiceImpl(mockDBOS); + + var date = LocalDate.of(2024, 1, 1); + + when(mockDBOS.runStep( + ArgumentMatchers.>any(), + ArgumentMatchers.eq("todaysDate"))) + .thenReturn(date); + when(mockDBOS.recv( + ArgumentMatchers.eq("greetTopic"), ArgumentMatchers.eq(Duration.ofSeconds(30)))) + .thenReturn("Alice"); + + // Call the workflow + String result = impl.testWorkflow(); + + // Verify output + assertEquals("Hello Alice, today is 2024-01-01", result); + + verify(mockDBOS) + .runStep( + ArgumentMatchers.>any(), + ArgumentMatchers.eq("todaysDate")); + verify(mockDBOS).setEvent(ArgumentMatchers.eq("greetEvent"), ArgumentMatchers.eq(date)); + verify(mockDBOS) + .recv(ArgumentMatchers.eq("greetTopic"), ArgumentMatchers.eq(Duration.ofSeconds(30))); + } +} diff --git a/transact/src/test/java/dev/dbos/transact/invocation/MultiClassInstanceTest.java b/transact/src/test/java/dev/dbos/transact/invocation/MultiClassInstanceTest.java new file mode 100644 index 00000000..e8a1523f --- /dev/null +++ b/transact/src/test/java/dev/dbos/transact/invocation/MultiClassInstanceTest.java @@ -0,0 +1,224 @@ +package dev.dbos.transact.invocation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import dev.dbos.transact.DBOS; +import dev.dbos.transact.DBOSClient; +import dev.dbos.transact.DBOSTestAccess; +import dev.dbos.transact.config.DBOSConfig; +import dev.dbos.transact.database.SystemDatabase; +import dev.dbos.transact.utils.DBUtils; +import dev.dbos.transact.workflow.ListWorkflowsInput; +import dev.dbos.transact.workflow.Queue; +import dev.dbos.transact.workflow.WorkflowState; + +import java.sql.SQLException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) +public class MultiClassInstanceTest { + private static DBOSConfig dbosConfig; + HawkServiceImpl himpl; + BearServiceImpl bimpla; + BearServiceImpl bimpl1; + private HawkService hproxy; + private BearService bproxya; + private BearService bproxy1; + private String localDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE); + + @BeforeAll + static void onetimeSetup() throws Exception { + dbosConfig = + DBOSConfig.defaultsFromEnv("systemdbtest") + .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_sys"); + } + + @BeforeEach + void beforeEachTest() throws SQLException { + DBUtils.recreateDB(dbosConfig); + DBOSTestAccess.reinitialize(dbosConfig); + himpl = new HawkServiceImpl(); + bimpla = new BearServiceImpl(); + bimpl1 = new BearServiceImpl(); + DBOS.registerQueue(new Queue("testQueue")); + + hproxy = DBOS.registerWorkflows(HawkService.class, himpl); + himpl.setProxy(hproxy); + + bproxya = DBOS.registerWorkflows(BearService.class, bimpla, "A"); + bimpla.setProxy(bproxya); + + bproxy1 = DBOS.registerWorkflows(BearService.class, bimpl1, "1"); + bimpl1.setProxy(bproxy1); + + DBOS.launch(); + } + + @AfterEach + void afterEachTest() throws Exception { + DBOS.shutdown(); + } + + @Test + void startWorkflow() throws Exception { + var bhandlea = + DBOS.startWorkflow( + () -> { + return bproxya.stepWorkflow(); + }); + var bresulta = bhandlea.getResult(); + assertEquals( + localDate, + bresulta + .atZone(ZoneId.systemDefault()) // or another zone + .toLocalDate() + .format(DateTimeFormatter.ISO_DATE)); + assertEquals(1, bimpla.nWfCalls); + + var bhandle1 = + DBOS.startWorkflow( + () -> { + return bproxy1.stepWorkflow(); + }); + var bresult1 = bhandle1.getResult(); + assertEquals( + localDate, + bresult1 + .atZone(ZoneId.systemDefault()) // or another zone + .toLocalDate() + .format(DateTimeFormatter.ISO_DATE)); + assertEquals(1, bimpl1.nWfCalls); + + var hhandle = + DBOS.startWorkflow( + () -> { + return hproxy.stepWorkflow(); + }); + var hresult = hhandle.getResult(); + assertEquals( + localDate, + hresult + .atZone(ZoneId.systemDefault()) // or another zone + .toLocalDate() + .format(DateTimeFormatter.ISO_DATE)); + + var browsa = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowId(bhandlea.workflowId())); + assertEquals(1, browsa.size()); + var browa = browsa.get(0); + assertEquals(bhandlea.workflowId(), browa.workflowId()); + assertEquals("stepWorkflow", browa.name()); + assertEquals("A", browa.instanceName()); + assertEquals("dev.dbos.transact.invocation.BearServiceImpl", browa.className()); + assertEquals("SUCCESS", browa.status()); + + var brows1 = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowId(bhandle1.workflowId())); + assertEquals(1, brows1.size()); + var brow1 = brows1.get(0); + assertEquals(bhandle1.workflowId(), brow1.workflowId()); + assertEquals("stepWorkflow", brow1.name()); + assertEquals("1", brow1.instanceName()); + assertEquals("dev.dbos.transact.invocation.BearServiceImpl", brow1.className()); + assertEquals("SUCCESS", brow1.status()); + + var hrows = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowId(hhandle.workflowId())); + assertEquals(1, hrows.size()); + var hrow = hrows.get(0); + assertEquals(hhandle.workflowId(), hrow.workflowId()); + assertEquals("stepWorkflow", hrow.name()); + assertEquals("dev.dbos.transact.invocation.HawkServiceImpl", hrow.className()); + assertEquals("", hrow.instanceName()); + assertEquals("SUCCESS", hrow.status()); + + // All 3 w/ the same WF name + var allrows = DBOS.listWorkflows(new ListWorkflowsInput().withWorkflowName("stepWorkflow")); + assertEquals(3, allrows.size()); + + // 2 from BSI + var brows = + DBOS.listWorkflows( + new ListWorkflowsInput() + .withWorkflowName("stepWorkflow") + .withClassName("dev.dbos.transact.invocation.BearServiceImpl")); + assertEquals(2, brows.size()); + + // 2 from BSI + var browsjust1 = + DBOS.listWorkflows( + new ListWorkflowsInput() + .withWorkflowName("stepWorkflow") + .withClassName("dev.dbos.transact.invocation.BearServiceImpl") + .withInstanceName("1")); + assertEquals(1, browsjust1.size()); + } + + private static final String dbUrl = "jdbc:postgresql://localhost:5432/dbos_java_sys"; + private static final String dbUser = "postgres"; + private static final String dbPassword = System.getenv("PGPASSWORD"); + + @Test + public void enqueueForSpecificInstance() throws Exception { + try (var client = new DBOSClient(dbUrl, dbUser, dbPassword)) { + var options = + new DBOSClient.EnqueueOptions( + "dev.dbos.transact.invocation.BearServiceImpl", "stepWorkflow", "testQueue") + .withInstanceName("A"); + var handle = client.enqueueWorkflow(options, new Object[] {}); + + var result = handle.getResult(); + assertEquals( + localDate, + result + .atZone(ZoneId.systemDefault()) // or another zone + .toLocalDate() + .format(DateTimeFormatter.ISO_DATE)); + + assertEquals(1, bimpla.nWfCalls); + assertEquals(0, bimpl1.nWfCalls); + + var stat = client.getWorkflowStatus(handle.workflowId()); + assertEquals( + "SUCCESS", + stat.orElseThrow(() -> new AssertionError("Workflow status not found")).status()); + + try (var dataSource = SystemDatabase.createDataSource(dbosConfig)) { + DBUtils.setWorkflowState(dataSource, handle.workflowId(), WorkflowState.PENDING.name()); + } + stat = client.getWorkflowStatus(handle.workflowId()); + assertEquals( + "PENDING", + stat.orElseThrow(() -> new AssertionError("Workflow status not found")).status()); + + var dbosExecutor = DBOSTestAccess.getDbosExecutor(); + var eh = dbosExecutor.executeWorkflowById(handle.workflowId(), false, true); + eh.getResult(); + stat = client.getWorkflowStatus(handle.workflowId()); + assertEquals( + "SUCCESS", + stat.orElseThrow(() -> new AssertionError("Workflow status not found")).status()); + assertEquals(0, bimpl1.nWfCalls); + assertEquals(2, bimpla.nWfCalls); + } + } + + @Test + void listSteps() throws Exception { + var bh = DBOS.startWorkflow(() -> bproxya.stepWorkflow()); + bh.getResult(); + var sh = DBOS.startWorkflow(() -> bproxya.listSteps(bh.workflowId())); + var ss = sh.getResult(); + assertEquals("1 1", ss); + + var steps = DBOS.listWorkflowSteps(sh.workflowId()); + assertEquals(2, steps.size()); + assertEquals("DBOS.listWorkflows", steps.get(0).functionName()); + assertEquals("DBOS.listWorkflowSteps", steps.get(1).functionName()); + } +} diff --git a/transact/src/test/java/dev/dbos/transact/invocation/MultiDbosInstanceTest.java b/transact/src/test/java/dev/dbos/transact/invocation/MultiDbosInstanceTest.java new file mode 100644 index 00000000..f1cdfacd --- /dev/null +++ b/transact/src/test/java/dev/dbos/transact/invocation/MultiDbosInstanceTest.java @@ -0,0 +1,151 @@ +package dev.dbos.transact.invocation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.dbos.transact.DBOS; +import dev.dbos.transact.StartWorkflowOptions; +import dev.dbos.transact.config.DBOSConfig; +import dev.dbos.transact.context.WorkflowOptions; +import dev.dbos.transact.utils.DBUtils; +import dev.dbos.transact.workflow.Queue; +import dev.dbos.transact.workflow.Workflow; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +interface TestService { + String testWorkflow(String name); +} + +class TestServiceImpl implements TestService { + private final DBOS.Instance dbos; + + public TestServiceImpl(DBOS.Instance instance) { + this.dbos = instance; + } + + @Workflow + public String testWorkflow(String name) { + var today = dbos.runStep(() -> LocalDate.now(), "todaysDate"); + return String.format("Hello %s, today is %s", name, today.format(DateTimeFormatter.ISO_DATE)); + } +} + +@org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) +public class MultiDbosInstanceTest { + + private static DBOSConfig dbosConfigA; + private DBOS.Instance dbosA; + private TestService proxyA; + private TestServiceImpl implA; + private Queue queueA; + + private static DBOSConfig dbosConfigB; + private DBOS.Instance dbosB; + private TestServiceImpl implB; + private TestService proxyB; + private Queue queueB; + + @BeforeAll + static void onetimeSetup() throws Exception { + dbosConfigA = + DBOSConfig.defaultsFromEnv("MultiDbosInstanceTestA") + .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_multi_a"); + dbosConfigB = + DBOSConfig.defaultsFromEnv("MultiDbosInstanceTestB") + .withDatabaseUrl("jdbc:postgresql://localhost:5432/dbos_java_multi_b"); + } + + @BeforeEach + void beforeEachTest() throws Exception { + DBUtils.recreateDB(dbosConfigA); + dbosA = new DBOS.Instance(dbosConfigA); + implA = new TestServiceImpl(dbosA); + proxyA = dbosA.registerWorkflows(TestService.class, implA); + queueA = new Queue("queueA"); + dbosA.registerQueue(queueA); + + dbosA.launch(); + + DBUtils.recreateDB(dbosConfigB); + dbosB = new DBOS.Instance(dbosConfigB); + implB = new TestServiceImpl(dbosB); + proxyB = dbosB.registerWorkflows(TestService.class, implB); + queueB = new Queue("queueB"); + dbosB.registerQueue(queueB); + dbosB.launch(); + } + + @AfterEach + void afterEachTest() throws Exception { + dbosA.shutdown(); + dbosB.shutdown(); + } + + @Test + public void testDirectMultipleInstances() throws Exception { + var wfidA = UUID.randomUUID().toString(); + String resultA; + try (var o = new WorkflowOptions(wfidA).setContext()) { + resultA = proxyA.testWorkflow("hawk"); + } + + var wfidB = UUID.randomUUID().toString(); + String resultB; + try (var o = new WorkflowOptions(wfidB).setContext()) { + resultB = proxyB.testWorkflow("bear"); + } + + String formattedCurrentDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE); + assertEquals("Hello hawk, today is " + formattedCurrentDate, resultA); + assertEquals("Hello bear, today is " + formattedCurrentDate, resultB); + + var rowsA = DBUtils.getWorkflowRows(dbosConfigA); + var rowsB = DBUtils.getWorkflowRows(dbosConfigB); + assertEquals(1, rowsA.size()); + assertEquals(1, rowsB.size()); + assertEquals(wfidA, rowsA.get(0).workflowId()); + assertEquals(wfidB, rowsB.get(0).workflowId()); + } + + @Test + public void testEnqueueMultipleInstances() throws Exception { + var handleA = + dbosA.startWorkflow(() -> proxyA.testWorkflow("hawk"), new StartWorkflowOptions(queueA)); + var handleB = + dbosB.startWorkflow(() -> proxyB.testWorkflow("bear"), new StartWorkflowOptions(queueB)); + + String formattedCurrentDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE); + assertEquals("Hello hawk, today is " + formattedCurrentDate, handleA.getResult()); + assertEquals("Hello bear, today is " + formattedCurrentDate, handleB.getResult()); + + var rowsA = DBUtils.getWorkflowRows(dbosConfigA); + var rowsB = DBUtils.getWorkflowRows(dbosConfigB); + assertEquals(1, rowsA.size()); + assertEquals(1, rowsB.size()); + assertEquals(handleA.workflowId(), rowsA.get(0).workflowId()); + assertEquals(handleB.workflowId(), rowsB.get(0).workflowId()); + } + + @Test + public void cantStartOnWrongInstance() throws Exception { + assertThrows( + IllegalStateException.class, () -> dbosA.startWorkflow(() -> proxyB.testWorkflow("bear"))); + } + + @Test + public void cantEnqueueOnWrongQueueInstance() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + dbosA.startWorkflow( + () -> proxyA.testWorkflow("hawk"), new StartWorkflowOptions(queueB))); + } +} From 3da64780db2b9b15dade99dffa156f7ad0ea947b Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 14:42:11 -0800 Subject: [PATCH 14/16] Revert "flow DBOSExecutor thru DBOSContext" This reverts commit 7e804888f23b44b1aa053cd3fad5e1e63adf239a. --- .../src/main/java/dev/dbos/transact/DBOS.java | 368 ++++++++---------- .../dbos/transact/context/DBOSContext.java | 25 +- .../dbos/transact/execution/DBOSExecutor.java | 1 - .../transact/execution/ThrowingRunnable.java | 7 - .../dev/dbos/transact/DBOSExtensions.kt | 2 - .../transact/invocation/InstanceTest.java | 91 ++++- 6 files changed, 263 insertions(+), 231 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index e823811d..cab91904 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -910,56 +910,6 @@ public static void shutdown() { ensureInstance().shutdown(); } - /** - * Get all workflows registered with DBOS. - * - * @return list of all registered workflow methods - */ - public static @NonNull Collection getRegisteredWorkflows() { - // TODO: no step wrapping in executor - return ensureInstance().getRegisteredWorkflows(); - } - - /** - * Get all workflow classes registered with DBOS. - * - * @return list of all class instances containing registered workflow methods - */ - public static @NonNull Collection getRegisteredWorkflowInstances() { - // TODO: no step wrapping in executor - return ensureInstance().getRegisteredWorkflowInstances(); - } - - /** - * Retrieve a queue definition - * - * @param queueName Name of the queue - * @return Queue definition for given `queueName` - */ - public static @NonNull Optional getQueue(@NonNull String queueName) { - // TODO: no step wrapping in executor - var executor = DBOSContext.dbosExecutor(); - return executor != null ? executor.getQueue(queueName) : ensureInstance().getQueue(queueName); - } - - /** - * Retrieve a handle to a workflow, given its ID. Note that a handle is always returned, whether - * the workflow exists or not; getStatus() can be used to tell the difference - * - * @param Return type of the workflow function - * @param Checked exception thrown by the workflow function, if any - * @param workflowId ID of the workflow to retrieve - * @return Workflow handle for the provided workflow ID - */ - public static @NonNull WorkflowHandle retrieveWorkflow( - @NonNull String workflowId) { - // TODO: no step wrapping in executor - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.retrieveWorkflow(workflowId) - : ensureInstance().retrieveWorkflow(workflowId); - } - /** * Get the ID of the current running workflow, or `null` if a workflow is not in progress * @@ -1003,15 +953,36 @@ public static boolean inStep() { } /** - * Start or enqueue a workflow with no return value, using default options + * Retrieve a queue definition + * + * @param queueName Name of the queue + * @return Queue definition for given `queueName` + */ + public static @NonNull Optional getQueue(@NonNull String queueName) { + return ensureInstance().getQueue(queueName); + } + + /** + * Durable sleep. Use this instead of Thread.sleep, especially in workflows. On restart or during + * recovery the original expected wakeup time is honoured as opposed to sleeping all over again. + * + * @param duration amount of time to sleep + */ + public static void sleep(@NonNull Duration duration) { + ensureInstance().sleep(duration); + } + + /** + * Start or enqueue a workflow with default options * + * @param Return type of the workflow * @param Type of checked exception thrown by the workflow, if any - * @param runnable A lambda that calls exactly one workflow function + * @param supplier A lambda that calls exactly one workflow function * @return A handle to the enqueued or running workflow */ - public static @NonNull WorkflowHandle startWorkflow( - @NonNull ThrowingRunnable runnable) { - return DBOS.startWorkflow(runnable, new StartWorkflowOptions()); + public static @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingSupplier supplier) { + return ensureInstance().startWorkflow(supplier, new StartWorkflowOptions()); } /** @@ -1024,20 +995,19 @@ public static boolean inStep() { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingRunnable runnable, @NonNull StartWorkflowOptions options) { - return DBOS.startWorkflow(runnable.asSupplier(), options); + return ensureInstance().startWorkflow(runnable, options); } /** - * Start or enqueue a workflow with default options + * Start or enqueue a workflow with no return value, using default options * - * @param Return type of the workflow * @param Type of checked exception thrown by the workflow, if any - * @param supplier A lambda that calls exactly one workflow function + * @param runnable A lambda that calls exactly one workflow function * @return A handle to the enqueued or running workflow */ - public static @NonNull WorkflowHandle startWorkflow( - @NonNull ThrowingSupplier supplier) { - return DBOS.startWorkflow(supplier, new StartWorkflowOptions()); + public static @NonNull WorkflowHandle startWorkflow( + @NonNull ThrowingRunnable runnable) { + return ensureInstance().startWorkflow(runnable, new StartWorkflowOptions()); } /** @@ -1051,65 +1021,44 @@ public static boolean inStep() { */ public static @NonNull WorkflowHandle startWorkflow( @NonNull ThrowingSupplier supplier, @NonNull StartWorkflowOptions options) { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.startWorkflow(supplier, options) - : ensureInstance().startWorkflow(supplier, options); - } - - /** - * Run the provided function as a step; this variant is for functions with no return value - * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param name Name of the step, for tracing and recording in the system database - * @throws E - */ - public static void runStep( - @NonNull ThrowingRunnable stepfunc, @NonNull String name) throws E { - DBOS.runStep(stepfunc, new StepOptions(name)); + return ensureInstance().startWorkflow(supplier, options); } /** - * Run the provided function as a step; this variant is for functions with no return value + * Execute a workflow based on registration and arguments. This is expected to be used by event + * listeners, not app code. * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param opts step name, and retry options for running the step - * @throws E + * @param regWorkflow Registration of the workflow. @see getRegisteredWorkflows + * @param args Workflow function arguments + * @param options Execution options, such as ID, queue, and timeout/deadline + * @return WorkflowHandle to the executed workflow */ - public static void runStep( - @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { - DBOS.runStep(stepfunc.asSupplier(), opts); + public static WorkflowHandle startWorkflow( + RegisteredWorkflow regWorkflow, Object[] args, StartWorkflowOptions options) { + return ensureInstance().startWorkflow(regWorkflow, args, options); } /** - * Run the provided function as a step; this variant is for functions with a return value + * Get the result of a workflow, or rethrow the exception thrown by the workflow * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param name name of the step, for tracing and to record in the system database - * @throws E + * @param Return type of the workflow + * @param Checked exception type, if any, thrown by the workflow + * @param workflowId ID of the workflow to retrieve + * @return Return value of the workflow + * @throws E if the workflow threw an exception */ - public static T runStep( - @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { - return DBOS.runStep(stepfunc, new StepOptions(name)); + public static T getResult(@NonNull String workflowId) throws E { + return ensureInstance().getResult(workflowId); } /** - * Run the provided function as a step; this variant is for functions with a return value + * Get the status of a workflow * - * @param Checked exception thrown by the step, if any - * @param stepfunc function or lambda to run - * @param opts step name, and retry options for running the step - * @throws E + * @param workflowId ID of the workflow to query + * @return Current workflow status for the provided workflowId, or null. */ - public static T runStep( - @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.runStepInternal(stepfunc, opts, null) - : ensureInstance().runStep(stepfunc, opts); + public static @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { + return ensureInstance().getWorkflowStatus(workflowId); } /** @@ -1125,7 +1074,7 @@ public static void send( @NonNull Object message, @Nullable String topic, @Nullable String idempotencyKey) { - DBOS.send(destinationId, message, topic, idempotencyKey, null); + ensureInstance().send(destinationId, message, topic, idempotencyKey); } /** @@ -1137,7 +1086,7 @@ public static void send( */ public static void send( @NonNull String destinationId, @NonNull Object message, @NonNull String topic) { - DBOS.send(destinationId, message, topic, null, null); + ensureInstance().send(destinationId, message, topic); } /** @@ -1155,12 +1104,7 @@ public static void send( @Nullable String topic, @Nullable String idempotencyKey, @Nullable SerializationStrategy serialization) { - var executor = DBOSContext.dbosExecutor(); - if (executor != null) { - executor.send(destinationId, message, topic, idempotencyKey, serialization); - } else { - ensureInstance().send(destinationId, message, topic, idempotencyKey, serialization); - } + ensureInstance().send(destinationId, message, topic, idempotencyKey, serialization); } /** @@ -1171,8 +1115,7 @@ public static void send( * @return the message if there is one or else null */ public static @Nullable Object recv(@Nullable String topic, @NonNull Duration timeout) { - var executor = DBOSContext.dbosExecutor(); - return executor != null ? executor.recv(topic, timeout) : ensureInstance().recv(topic, timeout); + return ensureInstance().recv(topic, timeout); } /** @@ -1182,7 +1125,7 @@ public static void send( * @param value data that is published */ public static void setEvent(@NonNull String key, @NonNull Object value) { - DBOS.setEvent(key, value, null); + ensureInstance().setEvent(key, value); } /** @@ -1194,12 +1137,7 @@ public static void setEvent(@NonNull String key, @NonNull Object value) { */ public static void setEvent( @NonNull String key, @NonNull Object value, @Nullable SerializationStrategy serialization) { - var executor = DBOSContext.dbosExecutor(); - if (executor != null) { - executor.setEvent(key, value, serialization); - } else { - ensureInstance().setEvent(key, value, serialization); - } + ensureInstance().setEvent(key, value, serialization); } /** @@ -1212,39 +1150,61 @@ public static void setEvent( */ public static @Nullable Object getEvent( @NonNull String workflowId, @NonNull String key, @NonNull Duration timeout) { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.getEvent(workflowId, key, timeout) - : ensureInstance().getEvent(workflowId, key, timeout); + return ensureInstance().getEvent(workflowId, key, timeout); } /** - * Get the result of a workflow, or rethrow the exception thrown by the workflow + * Run the provided function as a step; this variant is for functions with a return value * - * @param Return type of the workflow - * @param Checked exception type, if any, thrown by the workflow - * @param workflowId ID of the workflow to retrieve - * @return Return value of the workflow - * @throws E if the workflow threw an exception + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param name name of the step, for tracing and to record in the system database + * @throws E */ - public static T getResult(@NonNull String workflowId) throws E { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.getResult(workflowId) - : ensureInstance().getResult(workflowId); + public static T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull String name) throws E { + + return ensureInstance().runStep(stepfunc, name); } /** - * Get the status of a workflow + * Run the provided function as a step; this variant is for functions with no return value * - * @param workflowId ID of the workflow to query - * @return Current workflow status for the provided workflowId, or null. + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E */ - public static @Nullable WorkflowStatus getWorkflowStatus(@NonNull String workflowId) { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.getWorkflowStatus(workflowId) - : ensureInstance().getWorkflowStatus(workflowId); + public static void runStep( + @NonNull ThrowingRunnable stepfunc, @NonNull StepOptions opts) throws E { + ensureInstance().runStep(stepfunc, opts); + } + + /** + * Run the provided function as a step; this variant is for functions with no return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param name Name of the step, for tracing and recording in the system database + * @throws E + */ + public static void runStep( + @NonNull ThrowingRunnable stepfunc, @NonNull String name) throws E { + ensureInstance().runStep(stepfunc, name); + } + + /** + * Run the provided function as a step; this variant is for functions with a return value + * + * @param Checked exception thrown by the step, if any + * @param stepfunc function or lambda to run + * @param opts step name, and retry options for running the step + * @throws E + */ + public static T runStep( + @NonNull ThrowingSupplier stepfunc, @NonNull StepOptions opts) throws E { + + return ensureInstance().runStep(stepfunc, opts); } /** @@ -1257,10 +1217,7 @@ public static T getResult(@NonNull String workflowId) t */ public static @NonNull WorkflowHandle resumeWorkflow( @NonNull String workflowId) { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.resumeWorkflow(workflowId) - : ensureInstance().resumeWorkflow(workflowId); + return ensureInstance().resumeWorkflow(workflowId); } /*** @@ -1271,12 +1228,7 @@ public static T getResult(@NonNull String workflowId) t * @param workflowId ID of the workflow to cancel */ public static void cancelWorkflow(@NonNull String workflowId) { - var executor = DBOSContext.dbosExecutor(); - if (executor != null) { - executor.cancelWorkflow(workflowId); - } else { - ensureInstance().cancelWorkflow(workflowId); - } + ensureInstance().cancelWorkflow(workflowId); } /** @@ -1287,11 +1239,12 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @param Checked exception thrown by the workflow function, if any * @param workflowId Original workflow Id * @param startStep Start execution from this step. Prior steps copied over + * @param options {@link ForkOptions} containing forkedWorkflowId, applicationVersion, timeout * @return handle to the workflow */ public static @NonNull WorkflowHandle forkWorkflow( - @NonNull String workflowId, int startStep) { - return DBOS.forkWorkflow(workflowId, startStep, null); + @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { + return ensureInstance().forkWorkflow(workflowId, startStep, options); } /** @@ -1302,15 +1255,11 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @param Checked exception thrown by the workflow function, if any * @param workflowId Original workflow Id * @param startStep Start execution from this step. Prior steps copied over - * @param options {@link ForkOptions} containing forkedWorkflowId, applicationVersion, timeout * @return handle to the workflow */ public static @NonNull WorkflowHandle forkWorkflow( - @NonNull String workflowId, int startStep, @NonNull ForkOptions options) { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.forkWorkflow(workflowId, startStep, options) - : ensureInstance().forkWorkflow(workflowId, startStep, options); + @NonNull String workflowId, int startStep) { + return ensureInstance().forkWorkflow(workflowId, startStep); } /** @@ -1320,7 +1269,7 @@ public static void cancelWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public static void deleteWorkflow(@NonNull String workflowId) { - DBOS.deleteWorkflow(workflowId, false); + ensureInstance().deleteWorkflow(workflowId); } /** @@ -1332,12 +1281,21 @@ public static void deleteWorkflow(@NonNull String workflowId) { * @throws IllegalArgumentException if workflowId is null */ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChildren) { - var executor = DBOSContext.dbosExecutor(); - if (executor != null) { - executor.deleteWorkflow(workflowId, deleteChildren); - } else { - ensureInstance().deleteWorkflow(workflowId, deleteChildren); - } + ensureInstance().deleteWorkflow(workflowId, deleteChildren); + } + + /** + * Retrieve a handle to a workflow, given its ID. Note that a handle is always returned, whether + * the workflow exists or not; getStatus() can be used to tell the difference + * + * @param Return type of the workflow function + * @param Checked exception thrown by the workflow function, if any + * @param workflowId ID of the workflow to retrieve + * @return Workflow handle for the provided workflow ID + */ + public static @NonNull WorkflowHandle retrieveWorkflow( + @NonNull String workflowId) { + return ensureInstance().retrieveWorkflow(workflowId); } /** @@ -1347,8 +1305,7 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return a list of workflow status {@link WorkflowStatus} */ public static @NonNull List listWorkflows(@NonNull ListWorkflowsInput input) { - var executor = DBOSContext.dbosExecutor(); - return executor != null ? executor.listWorkflows(input) : ensureInstance().listWorkflows(input); + return ensureInstance().listWorkflows(input); } /** @@ -1358,24 +1315,51 @@ public static void deleteWorkflow(@NonNull String workflowId, boolean deleteChil * @return list of step information {@link StepInfo} */ public static @NonNull List listWorkflowSteps(@NonNull String workflowId) { - var executor = DBOSContext.dbosExecutor(); - return executor != null - ? executor.listWorkflowSteps(workflowId) - : ensureInstance().listWorkflowSteps(workflowId); + return ensureInstance().listWorkflowSteps(workflowId); } /** - * Durable sleep. Use this instead of Thread.sleep, especially in workflows. On restart or during - * recovery the original expected wakeup time is honoured as opposed to sleeping all over again. + * Get all workflows registered with DBOS. * - * @param duration amount of time to sleep + * @return list of all registered workflow methods */ - public static void sleep(@NonNull Duration duration) { - var executor = DBOSContext.dbosExecutor(); - if (executor == null) { - throw new IllegalStateException("DBOS.sleep must be called from a workflow"); - } - executor.sleep(duration); + public static @NonNull Collection getRegisteredWorkflows() { + return ensureInstance().getRegisteredWorkflows(); + } + + /** + * Get all workflow classes registered with DBOS. + * + * @return list of all class instances containing registered workflow methods + */ + public static @NonNull Collection getRegisteredWorkflowInstances() { + return ensureInstance().getRegisteredWorkflowInstances(); + } + + /** + * Get a system database record stored by an external service A unique value is stored per + * combination of service, workflowName, and key + * + * @param service Identity of the service maintaining the record + * @param workflowName Fully qualified name of the workflow + * @param key Key assigned within the service+workflow + * @return Value associated with the service+workflow+key combination + */ + public static Optional getExternalState( + String service, String workflowName, String key) { + return ensureInstance().getExternalState(service, workflowName, key); + } + + /** + * Insert or update a system database record stored by an external service A timestamped unique + * value is stored per combination of service, workflowName, and key + * + * @param state ExternalState containing the service, workflow, key, and value to store + * @return Value associated with the service+workflow+key combination, in case the stored value + * already had a higher version or timestamp + */ + public static ExternalState upsertExternalState(ExternalState state) { + return ensureInstance().upsertExternalState(state); } /** @@ -1393,11 +1377,7 @@ public static void sleep(@NonNull Duration duration) { * workflow */ public static boolean patch(@NonNull String patchName) { - var executor = DBOSContext.dbosExecutor(); - if (executor == null) { - throw new IllegalStateException("DBOS.patch must be called from a workflow"); - } - return executor.patch(patchName); + return ensureInstance().patch(patchName); } /** @@ -1413,10 +1393,6 @@ public static boolean patch(@NonNull String patchName) { * workflow */ public static boolean deprecatePatch(@NonNull String patchName) { - var executor = DBOSContext.dbosExecutor(); - if (executor == null) { - throw new IllegalStateException("DBOS.deprecatePatch must be called from a workflow"); - } - return executor.deprecatePatch(patchName); + return ensureInstance().deprecatePatch(patchName); } } diff --git a/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java b/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java index b8e2e3dd..499e69b1 100644 --- a/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java +++ b/transact/src/main/java/dev/dbos/transact/context/DBOSContext.java @@ -1,7 +1,6 @@ package dev.dbos.transact.context; import dev.dbos.transact.StartWorkflowOptions; -import dev.dbos.transact.execution.DBOSExecutor; import dev.dbos.transact.workflow.SerializationStrategy; import dev.dbos.transact.workflow.Timeout; @@ -11,8 +10,6 @@ public class DBOSContext { - DBOSExecutor dbosExecutor; - // assigned context options String nextWorkflowId; Timeout nextTimeout; @@ -30,7 +27,6 @@ public class DBOSContext { // private StepStatus stepStatus; public DBOSContext() { - dbosExecutor = null; workflowId = null; functionId = -1; parent = null; @@ -39,23 +35,16 @@ public DBOSContext() { serialization = SerializationStrategy.DEFAULT; } - public DBOSContext( - DBOSExecutor dbosExecutor, - String workflowId, - WorkflowInfo parent, - Duration timeout, - Instant deadline) { - this(dbosExecutor, workflowId, parent, timeout, deadline, null); + public DBOSContext(String workflowId, WorkflowInfo parent, Duration timeout, Instant deadline) { + this(workflowId, parent, timeout, deadline, null); } public DBOSContext( - DBOSExecutor dbosExecutor, String workflowId, WorkflowInfo parent, Duration timeout, Instant deadline, SerializationStrategy serialization) { - this.dbosExecutor = dbosExecutor; this.workflowId = workflowId; this.functionId = 0; this.parent = parent; @@ -69,7 +58,6 @@ public DBOSContext( StartWorkflowOptions options, Integer functionId, CompletableFuture future) { - this.dbosExecutor = other.dbosExecutor; this.nextWorkflowId = other.nextWorkflowId; this.nextTimeout = other.nextTimeout; this.nextDeadline = other.nextDeadline; @@ -156,10 +144,6 @@ public void setSerializationStrategy(SerializationStrategy strat) { this.serialization = strat; } - public DBOSExecutor getDbosExecutor() { - return dbosExecutor; - } - public static String workflowId() { var ctx = DBOSContextHolder.get(); return ctx == null ? null : ctx.workflowId; @@ -184,9 +168,4 @@ public static SerializationStrategy serializationStrategy() { var ctx = DBOSContextHolder.get(); return ctx != null ? ctx.getSerialization() : null; } - - public static DBOSExecutor dbosExecutor() { - var ctx = DBOSContextHolder.get(); - return ctx != null ? ctx.getDbosExecutor() : null; - } } diff --git a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java index bf832768..475d1ee4 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java +++ b/transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java @@ -1350,7 +1350,6 @@ private WorkflowHandle executeWorkflow( DBOSContextHolder.set( new DBOSContext( - this, workflowId, parent, foptions.timeoutDuration(), diff --git a/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java b/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java index c41094a4..6d382714 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java +++ b/transact/src/main/java/dev/dbos/transact/execution/ThrowingRunnable.java @@ -3,11 +3,4 @@ @FunctionalInterface public interface ThrowingRunnable { void execute() throws E; - - default ThrowingSupplier asSupplier() { - return () -> { - this.execute(); - return null; - }; - } } diff --git a/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt b/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt index 5b072dc5..5d174be1 100644 --- a/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt +++ b/transact/src/main/kotlin/dev/dbos/transact/DBOSExtensions.kt @@ -157,13 +157,11 @@ fun getRegisteredWorkflows() = DBOS.getRegisteredWorkflows() @JvmSynthetic fun getRegisteredWorkflowInstances() = DBOS.getRegisteredWorkflowInstances() -/** @JvmSynthetic fun getExternalState(service: String, workflowName: String, key: String) = DBOS.getExternalState(service, workflowName, key) @JvmSynthetic fun upsertExternalState(state: ExternalState) = DBOS.upsertExternalState(state) -*/ @JvmSynthetic fun patch(name: String) = DBOS.patch(name) diff --git a/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java b/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java index eb4bd5d6..9bbb3941 100644 --- a/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java +++ b/transact/src/test/java/dev/dbos/transact/invocation/InstanceTest.java @@ -12,7 +12,9 @@ import dev.dbos.transact.database.SystemDatabase; import dev.dbos.transact.exceptions.DBOSAwaitedWorkflowCancelledException; import dev.dbos.transact.utils.DBUtils; +import dev.dbos.transact.workflow.Step; import dev.dbos.transact.workflow.Timeout; +import dev.dbos.transact.workflow.Workflow; import java.sql.SQLException; import java.time.Duration; @@ -27,6 +29,88 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +class HawkServiceInstanceImpl implements HawkService { + private final DBOS.Instance dbos; + private HawkService proxy; + + public HawkServiceInstanceImpl(DBOS.Instance dbos) { + this.dbos = dbos; + } + + public void setProxy(HawkService proxy) { + this.proxy = proxy; + } + + @Workflow + @Override + public String simpleWorkflow() { + return LocalDate.now().format(DateTimeFormatter.ISO_DATE); + } + + @Workflow + @Override + public String sleepWorkflow(long sleepSec) { + var duration = Duration.ofSeconds(sleepSec); + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return LocalDate.now().format(DateTimeFormatter.ISO_DATE); + } + + @Workflow + @Override + public String parentWorkflow() { + return proxy.simpleWorkflow(); + } + + @Workflow + @Override + public String parentStartWorkflow() { + var handle = dbos.startWorkflow(() -> proxy.simpleWorkflow()); + return handle.getResult(); + } + + @Workflow + @Override + public String parentSleepWorkflow(Long timeoutSec, long sleepSec) { + var duration = + timeoutSec == null + ? Timeout.inherit() + : timeoutSec == 0L ? Timeout.none() : Timeout.of(Duration.ofSeconds(timeoutSec)); + var options = new WorkflowOptions().withTimeout(duration); + try (var o = options.setContext()) { + return proxy.sleepWorkflow(sleepSec); + } + } + + @Step + @Override + public Instant nowStep() { + return Instant.now(); + } + + @Workflow + @Override + public Instant stepWorkflow() { + return proxy.nowStep(); + } + + @Step + @Override + public String illegalStep() { + return proxy.simpleWorkflow(); + } + + @Workflow + @Override + public String illegalWorkflow() { + return proxy.illegalStep(); + } +} + @org.junit.jupiter.api.Timeout(value = 2, unit = java.util.concurrent.TimeUnit.MINUTES) public class InstanceTest { private static DBOSConfig dbosConfig; @@ -47,8 +131,11 @@ static void onetimeSetup() throws Exception { void beforeEachTest() throws SQLException { DBUtils.recreateDB(dbosConfig); + // Note, manually injecting the DBOS instance here is a poor developer experience + // Opened https://github.com/dbos-inc/dbos-transact-java/issues/296 to track improving this + dbos = new DBOS.Instance(dbosConfig); - var impl = new HawkServiceImpl(); + var impl = new HawkServiceInstanceImpl(dbos); proxy = dbos.registerWorkflows(HawkService.class, impl); impl.setProxy(proxy); @@ -75,7 +162,7 @@ void directInvoke() throws Exception { assertDoesNotThrow(() -> UUID.fromString((String) row.workflowId())); assertEquals("SUCCESS", row.status()); assertEquals("simpleWorkflow", row.name()); - assertEquals("dev.dbos.transact.invocation.HawkServiceImpl", row.className()); + assertEquals("dev.dbos.transact.invocation.HawkServiceInstanceImpl", row.className()); assertNotNull(row.output()); assertNull(row.error()); assertNull(row.timeoutMs()); From 6c00a5e81d0d659d0eca776d29a90304ff74ecff Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 17:41:27 -0800 Subject: [PATCH 15/16] update SchedulerService for better instance support --- .../transact/execution/SchedulerService.java | 140 +++++++++--------- 1 file changed, 73 insertions(+), 67 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java b/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java index 53f27eb6..cb17bb58 100644 --- a/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java +++ b/transact/src/main/java/dev/dbos/transact/execution/SchedulerService.java @@ -30,59 +30,58 @@ public class SchedulerService implements DBOSLifecycleListener { + record ScheduledWorkflow( + RegisteredWorkflow workflow, Cron cron, String queue, boolean ignoreMissed) {} + private static final Logger logger = LoggerFactory.getLogger(SchedulerService.class); private static final CronParser cronParser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING53)); + private static final Class[] expectedParams = new Class[] {Instant.class, Instant.class}; - private final String schedulerQueueName; - private DBOS.Instance dbos; - private final AtomicReference scheduler = new AtomicReference<>(); + private final String defaultSchedulerQueueName; + private final AtomicReference dbosRef = new AtomicReference<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4); - public SchedulerService(String defSchedulerQueue) { - this.schedulerQueueName = Objects.requireNonNull(defSchedulerQueue); + public SchedulerService(String defaultSchedulerQueueName) { + this.defaultSchedulerQueueName = Objects.requireNonNull(defaultSchedulerQueueName); } public static void validateScheduledWorkflow(RegisteredWorkflow workflow) { var method = workflow.workflowMethod(); var skedTag = method.getAnnotation(Scheduled.class); - if (skedTag == null) { - return; - } + if (skedTag != null) { + var paramTypes = method.getParameterTypes(); + if (!Arrays.equals(paramTypes, expectedParams)) { + throw new IllegalArgumentException( + "Invalid signature for Scheduled workflow %s. Signature must be (Instant, Instant)" + .formatted(workflow.fullyQualifiedName())); + } - var expectedParams = new Class[] {Instant.class, Instant.class}; - var paramTypes = method.getParameterTypes(); - if (!Arrays.equals(paramTypes, expectedParams)) { - throw new IllegalArgumentException( - "Invalid signature for Scheduled workflow %s. Signature must be (Instant, Instant)" - .formatted(workflow.fullyQualifiedName())); + cronParser.parse(skedTag.cron()); } - - cronParser.parse(skedTag.cron()); } + @Override public void dbosLaunched(DBOS.Instance dbos) { - this.dbos = dbos; - if (this.scheduler.get() == null) { - var scheduler = Executors.newScheduledThreadPool(4); - if (this.scheduler.compareAndSet(null, scheduler)) { - startScheduledWorkflows(); - } + DBOS.Instance prev = this.dbosRef.getAndUpdate(existing -> existing == null ? dbos : existing); + if (prev == null) { + startScheduledWorkflows(dbos); + } else if (prev != dbos) { + throw new IllegalStateException( + "SchedulerService already initialized with a different DBOS instance"); } } + @Override public void dbosShutDown() { - this.dbos = null; - var scheduler = this.scheduler.getAndSet(null); - if (scheduler != null) { + DBOS.Instance prev = this.dbosRef.getAndSet(null); + if (prev != null) { List notRun = scheduler.shutdownNow(); logger.debug("Shutting down scheduler service. Tasks not run {}", notRun.size()); } } - record ScheduledWorkflow( - RegisteredWorkflow workflow, Cron cron, String queue, boolean ignoreMissed) {} - - private ZonedDateTime getLastTime(ScheduledWorkflow swf) { + private static ZonedDateTime getLastTime(DBOS.Instance dbos, ScheduledWorkflow swf) { if (!swf.ignoreMissed()) { var state = dbos.getExternalState( @@ -94,7 +93,8 @@ private ZonedDateTime getLastTime(ScheduledWorkflow swf) { return ZonedDateTime.now(ZoneOffset.UTC).withNano(0); } - private ZonedDateTime setLastTime(ScheduledWorkflow swf, ZonedDateTime lastTime) { + private static ZonedDateTime setLastTime( + DBOS.Instance dbos, ScheduledWorkflow swf, ZonedDateTime lastTime) { if (swf.ignoreMissed()) { return ZonedDateTime.now(ZoneOffset.UTC).withNano(0); } @@ -111,14 +111,11 @@ private ZonedDateTime setLastTime(ScheduledWorkflow swf, ZonedDateTime lastTime) return ZonedDateTime.parse(state.value()).plus(1, ChronoUnit.MILLIS); } - private void startScheduledWorkflows() { - logger.debug("startScheduledWorkflows"); - - var expectedParams = new Class[] {Instant.class, Instant.class}; - - // collect all workflows that have an @Scheduled annotation - List scheduledWorkflows = new ArrayList<>(); - for (var wf : dbos.getRegisteredWorkflows()) { + private static List getScheduledWorkflows( + DBOS.Instance dbos, String defaultSchedulerQueueName) { + var registeredWorkflows = dbos.getRegisteredWorkflows(); + var scheduledWorkflows = new ArrayList(); + for (var wf : registeredWorkflows) { var method = wf.workflowMethod(); var skedTag = method.getAnnotation(Scheduled.class); if (skedTag == null) { @@ -133,21 +130,25 @@ private void startScheduledWorkflows() { continue; } - String queue = - skedTag.queue() != null && !skedTag.queue().isEmpty() - ? skedTag.queue() - : this.schedulerQueueName; - var q = dbos.getQueue(queue); - if (!q.isPresent()) { + // fields of Java annotations can't be null. + // @Scheduled.queue defaults to empty string if not specified + // using requireNonNullElse here for safety purposes + var queueName = Objects.requireNonNullElse(skedTag.queue(), ""); + queueName = queueName.isEmpty() ? defaultSchedulerQueueName : queueName; + var queue = dbos.getQueue(queueName); + if (!queue.isPresent()) { logger.error( - "Scheduled workflow {} refers to undefined queue {}", wf.fullyQualifiedName(), queue); - queue = this.schedulerQueueName; + "Scheduled workflow {} refers to undefined queue {}", + wf.fullyQualifiedName(), + queueName); + continue; } try { var cron = cronParser.parse(skedTag.cron()); scheduledWorkflows.add( - new ScheduledWorkflow(wf, Objects.requireNonNull(cron), queue, skedTag.ignoreMissed())); + new ScheduledWorkflow( + wf, Objects.requireNonNull(cron), queueName, skedTag.ignoreMissed())); } catch (IllegalArgumentException e) { logger.error( "Scheduled workflow {} has invalid cron expression {}", @@ -155,32 +156,35 @@ private void startScheduledWorkflows() { skedTag.cron()); } } + return scheduledWorkflows; + } - for (var _swf : scheduledWorkflows) { + private void startScheduledWorkflows(DBOS.Instance dbos) { + logger.debug("startScheduledWorkflows"); + var scheduledWorkflows = getScheduledWorkflows(dbos, defaultSchedulerQueueName); + for (var _scheduledWorkflow : scheduledWorkflows) { + var _nextTime = getLastTime(dbos, _scheduledWorkflow); var task = new Runnable() { - final ScheduledWorkflow swf = _swf; - final ExecutionTime executionTime = ExecutionTime.forCron(swf.cron()); - final String workflowName = swf.workflow().fullyQualifiedName(); - - ZonedDateTime nextTime = getLastTime(swf); + final ScheduledWorkflow scheduledWorkflow = _scheduledWorkflow; + final ExecutionTime executionTime = ExecutionTime.forCron(scheduledWorkflow.cron()); + final String workflowName = scheduledWorkflow.workflow().fullyQualifiedName(); + ZonedDateTime nextTime = _nextTime; public void schedule() { executionTime .nextExecution(nextTime) .ifPresent( - nextTime -> { - this.nextTime = nextTime; + _nextTime -> { + this.nextTime = _nextTime; long initialDelayMs = - Duration.between(ZonedDateTime.now(ZoneOffset.UTC), nextTime) + Duration.between(ZonedDateTime.now(ZoneOffset.UTC), _nextTime) .toMillis(); // ensure scheduler hasn't been shutdown before scheduling - var localScheduler = scheduler.get(); - if (localScheduler != null) { - logger.debug("Scheduling {} @ {}", workflowName, nextTime); - - localScheduler.schedule( + if (dbosRef.get() != null) { + logger.debug("Scheduling {} @ {}", workflowName, _nextTime); + scheduler.schedule( this, initialDelayMs < 0 ? 0 : initialDelayMs, TimeUnit.MILLISECONDS); } }); @@ -188,9 +192,10 @@ public void schedule() { @Override public void run() { - // if scheduler service isn't running, the scheduler service was shut down so don't - // start the workflow or schedule the next execution - if (scheduler.get() == null) { + // if dbos is null, the scheduler service was shut down so don't start the workflow or + // schedule the next execution + var dbos = dbosRef.get(); + if (dbos == null) { return; } @@ -204,9 +209,10 @@ public void run() { String workflowId = String.format("sched-%s-%s", workflowName, scheduledTime.toString()); - var options = new StartWorkflowOptions(workflowId).withQueue(swf.queue()); - dbos.startWorkflow(swf.workflow(), args, options); - nextTime = setLastTime(swf, scheduledTime); + var options = + new StartWorkflowOptions(workflowId).withQueue(scheduledWorkflow.queue()); + dbos.startWorkflow(scheduledWorkflow.workflow(), args, options); + nextTime = setLastTime(dbos, scheduledWorkflow, scheduledTime); } catch (Exception e) { logger.error("Scheduled task exception {}", workflowName, e); } finally { From b0323f57750cf3aa484f6c8834e56bf8ca0073a1 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 5 Mar 2026 17:43:57 -0800 Subject: [PATCH 16/16] copilot feedback --- transact/src/main/java/dev/dbos/transact/DBOS.java | 4 ++-- .../dev/dbos/transact/internal/DBOSInvocationHandler.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/transact/src/main/java/dev/dbos/transact/DBOS.java b/transact/src/main/java/dev/dbos/transact/DBOS.java index cab91904..0a4dbba1 100644 --- a/transact/src/main/java/dev/dbos/transact/DBOS.java +++ b/transact/src/main/java/dev/dbos/transact/DBOS.java @@ -945,8 +945,8 @@ public static boolean inStep() { /** * Get the serialization format of the current workflow context. * - * @return the serialization format name (e.g., "portable_json", "java_jackson"), or null if not - * in a workflow context or using default serialization + * @return the SerializationStrategy (e.g., "portable_json", "java_jackson"), or null if not in a + * workflow context or using default serialization */ public static @Nullable SerializationStrategy serializationStrategy() { return DBOSContext.serializationStrategy(); diff --git a/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java b/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java index 19894fd7..c1d8b7ce 100644 --- a/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java +++ b/transact/src/main/java/dev/dbos/transact/internal/DBOSInvocationHandler.java @@ -46,6 +46,7 @@ public static T createProxy( new DBOSInvocationHandler(implementation, instanceName, executor)); } + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Exception { var implMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes()); @@ -98,6 +99,9 @@ protected Object handleWorkflow( : target.getClass().getName(); var workflowName = workflow.name().isEmpty() ? method.getName() : workflow.name(); var executor = executorSupplier.get(); + if (executor == null) { + throw new IllegalStateException("executorSupplier returned null"); + } if (hook != null) { var invocation = new Invocation(executor, className, instanceName, workflowName, args); @@ -105,10 +109,6 @@ protected Object handleWorkflow( return defaultReturn(method); } - if (executor == null) { - throw new IllegalStateException("executorSupplier returned null"); - } - var handle = executor.invokeWorkflow(className, instanceName, workflowName, args); // This is not really a getResult call - it is part of invocation which will be written