Skip to content

Latest commit

 

History

History
874 lines (697 loc) · 34.8 KB

File metadata and controls

874 lines (697 loc) · 34.8 KB

SmallMind Batch

The smallmind-batch module is a thin, opinionated facade over Spring Batch. It is organized as two artifacts:

  • batch-base — a framework-neutral parameter envelope and a JobFactory contract. No Spring dependency, no JDBC, no transitive Spring Batch classes. Safe to import from a service interface module, a shared model jar, or any library that wants to describe a batch invocation without pulling in the runtime that executes it.

  • batch-spring — the Spring Batch implementation. Translates the neutral parameter wrappers into Spring’s JobParameters, discovers Job beans from the application context, vends monitors and watchers for polling execution state, and ships a ready-to-import Spring XML configuration plus the Liquibase changelog that creates the BATCH_* metadata tables.

The module is small by design: six classes in batch-base (one enum, one interface, one abstract parameter, three typed subclasses, and a convenience map), eight classes in batch-spring, one Spring XML wiring file, and one Liquibase changelog. It exists to turn Spring Batch into something a service can adopt in an afternoon — without learning the full Spring Batch bean graph, without hand-writing schema DDL, and without coupling the service’s public API to Spring.

Spring Batch is powerful and comprehensive, but it has a shape problem for services whose batch work is a minor part of a larger application. A production Spring Batch setup needs, at minimum:

  1. A JobRepository (typically the JDBC-backed JobRepositoryFactoryBean), which in turn requires a DataSource and a PlatformTransactionManager.

  2. A JobOperator (or JobLauncher) with a TaskExecutor for asynchronous launches.

  3. A JobRegistry — or a disciplined convention for wiring Job beans so that the operator can find them by name.

  4. A migration tool or a hand-crafted schema script to create the BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_STEP_EXECUTION, and context tables plus the three sequences.

  5. A strategy for waiting on job completion, for monitoring exit status, and for restarting failed executions — none of which ships as a single plug-in-able component.

  6. A way to pass caller-supplied parameters into a job. Spring Batch’s JobParametersBuilder has a method per Java type (addLong, addString, addDouble, addLocalDateTime, …) and an identifying flag per entry, so code that accepts parameters from somewhere else (a REST controller, a CLI argument parser, a scheduled trigger) always ends up doing the translation by hand.

smallmind-batch assembles (1) through (5) once, as an importable Spring XML file plus a few collaborating beans, and solves (6) with a typed parameter envelope that is framework-neutral on the contract side and Spring-aware on the implementation side. A caller starts a job with two lines of code, waits on it with three more, and never touches JobParametersBuilder or JobRepositoryFactoryBean directly.

  • Defines a neutral parameter envelope (BatchParameter and the typed BatchParameters map) so callers can describe a job invocation without referencing any Spring Batch class.

  • Implements JobFactory against Spring Batch (BatchJobFactory), translating each BatchParameter entry into the correct JobParametersBuilder.addXxx call and delegating to a JobOperator.

  • Auto-discovers Job beans (BatchJobRegistry) by scanning the application context during BeanFactoryPostProcessor#postProcessBeanFactory — there is no @JobScope, no component-scan path, no manual register call. If a bean is assignable to Job, it is a job.

  • Polls for completion at three levels of granularity: a single execution (BatchJobMonitor), every execution visible through the repository with a configurable settling period (BatchJobWatcher), or every row in the BATCH_JOB_EXECUTION table read directly via JDBC (BatchJobInterrogator).

  • Ships the BATCH_* schema as Liquibase (Batch.changelog.xml), wired through smallmind-liquibase so that the tables and the three sequences are created or migrated as part of the same application startup that initializes the JobRepository.

  • Exposes a single task-executor factory bean (BatchJobExecutorFactory) so the operator’s concurrency limit is configured as a plain integer property on an XML bean, rather than by assembling an ExecutorService in Java.

  • It does not define jobs, steps, tasklets, or item readers/writers. Those remain ordinary Spring Batch beans that the caller authors. This module launches, registers, monitors, and restarts them; it does not describe them.

  • It does not ship a REST or JMX admin console. BatchJobFactory is a Java API; wrapping it in a transport is the caller’s decision.

  • It does not attempt to replace Spring Batch’s JobOperator or JobRepository. BatchJobFactory composes them; it does not reimplement them.

  • It does not support Spring Batch partitioning, remote chunking, or multi-node coordination. BatchJobWatcher and BatchJobInterrogator query the same JobRepository / metadata tables the jobs write to, so they work correctly when all executions run on one node. Cross-node orchestration is out of scope.

Class Role

JobFactory

Framework-neutral contract. Two methods: create(logicalName, parameterMap, reason) starts a job and returns the new execution id; restart(executionId) re-runs an existing execution. This is the type a caller should depend on when the caller is not itself a Spring Batch component.

BatchParameter<T>

Abstract envelope carrying a typed value plus an identifying flag. The flag decides whether the value contributes to the job-instance identity key — the hash that Spring Batch uses to distinguish one run from another.

DateBatchParameter / DoubleBatchParameter / LongBatchParameter / StringBatchParameter

Concrete subclasses, one per Spring Batch parameter flavor. Each pins BatchParameter.getType() to the matching ParameterType enum constant so the Spring implementation can dispatch on it without instanceof checks.

ParameterType

Discriminant enum with exactly four constants: DATE, DOUBLE, LONG, STRING. Mirrors the four JobParametersBuilder.addXxx methods supported by the translation layer.

BatchParameters

HashMap<String, BatchParameter<?>> with typed putDate, putDouble, putLong, putString helpers so callers never instantiate the subclasses directly. This is the type most application code should use to build up a parameter set.

Class Role

BatchJobFactory

Spring Batch implementation of JobFactory. Translates each BatchParameter to a JobParametersBuilder call, logs the launch with reason and parameter list, delegates to a JobOperator for start and restart, and vends BatchJobMonitor / BatchJobWatcher instances.

BatchJobRegistry

JobRegistry + BeanFactoryPostProcessor + ApplicationListener<ContextRefreshedEvent>. Scans bean definitions at post-processor time, records every bean assignable to Job, and resolves them lazily through the captured application context. Manual register / unregister calls throw UnsupportedOperationException: jobs must be declared as Spring beans.

BatchJobExecutorFactory

FactoryBean<TaskExecutorAdapter> wrapping a fixed-size ScheduledThreadPoolExecutor. Exposes a single property, concurrencyLimit, which is silently clamped to a minimum of 1. Intended as the taskExecutor property on SimpleJobOperator.

BatchJobMonitor

Polls a single JobExecution until its ExitStatus matches any of a supplied set, or until a timeout expires. Sleep interval is one-tenth of the total timeout, with a minimum of one second.

BatchJobWatcher

Polls every execution across every job instance and declares success when all executions are BatchStatus.COMPLETED and have stayed complete for a caller-supplied clear period. The clear period prevents false positives when a new execution is about to start.

BatchJobInterrogator

Same idea as BatchJobWatcher, but queries the BATCH_JOB_EXECUTION table directly with JDBC and does not require a JobRepository. Useful for external quiescence checks — e.g., a deployment gate that waits for all batch activity to stop before tearing the service down.

TaskletUtility

Static helper for tasklet bodies. One method today: asBoolean reads a string-valued job parameter and parses it as a boolean, falling back to a caller-supplied default when absent.

FormattedNoSuchJobException / FormattedJobRestartException

NoSuchJobException variants that accept String.format patterns so throw sites can embed the logical job name or execution id without a separate formatting step.

Resource Role

classpath:org/smallmind/batch/spring/batch.xml

Composition root. Defines jobRepository, jobRegistry, jobOperator, and jobFactory. Imports batch-liquibase.xml. This is the single <import resource="…​"/> most callers need.

classpath:org/smallmind/batch/spring/batch-liquibase.xml

Defines batchDataSource (a DriverManagerDataSource driven by four jdbc.*.batch properties) and batchLiquibase (a SpringLiquibase that runs the changelog on startup).

classpath:org/smallmind/batch/spring/Batch.changelog.xml

Liquibase changelog that creates BATCH_JOB_INSTANCE, BATCH_JOB_EXECUTION, BATCH_JOB_EXECUTION_PARAMS, BATCH_JOB_EXECUTION_CONTEXT, BATCH_STEP_EXECUTION, BATCH_STEP_EXECUTION_CONTEXT, and the three *_SEQ tables, plus the foreign keys, unique constraints, and indexes that Spring Batch expects.

A few ideas recur throughout the module and are worth naming up front.

Logical job name

The bean id under which a Job is declared in the Spring context. Passed to JobFactory.create(logicalName, …) and looked up through BatchJobRegistry.getJob(name). There is no separate registration step: the bean id is the logical name.

Job instance vs. job execution

A job instance is the abstract identity of a run, determined by the job name plus the identifying subset of its parameters. A job execution is a concrete attempt against that instance — first run, retry, restart. BatchJobFactory.create returns the new execution id; restart takes an execution id and produces a new one.

Identifying parameter

A parameter whose value contributes to the job instance identity key. Two launches with the same job name and the same identifying parameters target the same instance, which is why a duplicate launch with identical identifying parameters against a successfully completed instance throws JobInstanceAlreadyCompleteException. Non-identifying parameters are still delivered to the job but are not part of the hash.

Settling period (BatchJobWatcher)

The minimum duration for which every visible execution must have been complete before the watcher returns success. Without this, a watcher that polled right after one job completed but before the next started would wrongly declare quiescence. The period is a caller decision; a couple of seconds is usually enough to bridge scheduler pulse intervals.

The two artifacts are published under org.smallmind:

<dependency>
  <groupId>org.smallmind</groupId>
  <artifactId>batch-base</artifactId>
  <version>${project.version}</version>
</dependency>

<dependency>
  <groupId>org.smallmind</groupId>
  <artifactId>batch-spring</artifactId>
  <version>${project.version}</version>
</dependency>

A reusable library that only needs to describe a batch launch (e.g., a service API module whose REST controllers accept parameter maps and hand them off to the runtime) depends on batch-base only. An application that wants to actually run jobs depends on batch-spring, which transitively pulls in batch-base, spring-batch-core, smallmind-liquibase, smallmind-nutsnbolts, and scribe-pen for logging.

  • A JDBC data source addressable through four Spring properties: jdbc.driver.classname, jdbc.url.batch, jdbc.username.batch, and jdbc.password.batch. These are consumed by batch-liquibase.xml to build batchDataSource. If you want to reuse an existing DataSource bean instead, override the batchDataSource definition in your own context.

  • A database type understood by Spring Batch’s JobRepositoryFactoryBean. The shipped batch.xml sets databaseType=mysql; override the property or the bean definition if you target a different engine.

  • Liquibase on the classpath at runtime (smallmind-liquibase pulls it in transitively). The changelog is run automatically during context startup through the batchLiquibase bean.

The fastest path is to import batch.xml and define your jobs as ordinary beans.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:batch="http://www.springframework.org/schema/batch"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/batch
         http://www.springframework.org/schema/batch/spring-batch.xsd">

  <import resource="classpath:org/smallmind/batch/spring/batch.xml"/>  (1)

  <batch:job id="nightlyReconciliation">                                 (2)
    <batch:step id="reconcileStep">
      <batch:tasklet ref="reconciliationTasklet"/>
    </batch:step>
  </batch:job>

  <bean id="reconciliationTasklet"
        class="com.example.batch.ReconciliationTasklet"/>
</beans>
  1. Brings in jobRepository, jobRegistry, jobOperator, jobFactory, batchLiquibase, and batchDataSource.

  2. The bean id (nightlyReconciliation) is the logical name that BatchJobRegistry will discover and that jobFactory.create(…​) will accept. No further registration step is needed.

With that in place, any component that wants to launch the job takes a dependency on jobFactory and calls create:

import java.time.LocalDateTime;
import org.smallmind.batch.base.BatchParameters;
import org.smallmind.batch.base.JobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ReconciliationLauncher {

  private final JobFactory jobFactory;

  @Autowired
  public ReconciliationLauncher (JobFactory jobFactory) {

    this.jobFactory = jobFactory;
  }

  public long launchForToday ()
    throws Exception {

    BatchParameters parameters = new BatchParameters();

    parameters.putDate("runDate", LocalDateTime.now(), true);       (1)
    parameters.putString("dryRun", "false", false);                 (2)

    return jobFactory.create("nightlyReconciliation", parameters,   (3)
                             "scheduled nightly reconciliation");
  }
}
  1. Identifying: two launches with the same runDate hit the same job instance and the second will throw JobInstanceAlreadyCompleteException if the first has already finished.

  2. Non-identifying: the job receives the value, but it does not participate in the instance hash, so flipping it between runs does not create a new instance.

  3. The third argument is a free-form reason string written to the log. It is advisory only and may be null.

BatchJobFactory.monitor(long) returns a BatchJobMonitor that polls the execution until its ExitStatus matches one the caller cares about.

import java.util.concurrent.TimeUnit;
import org.smallmind.batch.base.BatchParameters;
import org.smallmind.batch.spring.BatchJobFactory;
import org.smallmind.batch.spring.BatchJobMonitor;
import org.springframework.batch.core.ExitStatus;

public class BlockingLauncher {

  private final BatchJobFactory factory;

  public BlockingLauncher (BatchJobFactory factory) {

    this.factory = factory;
  }

  public boolean launchAndWaitForCompletion (String jobName, BatchParameters params)
    throws Exception {

    long executionId = factory.create(jobName, params, "synchronous caller");

    BatchJobMonitor monitor = factory.monitor(executionId);

    return monitor.await(5, TimeUnit.MINUTES,                           (1)
                         ExitStatus.COMPLETED, ExitStatus.FAILED);      (2)
  }
}
  1. Polling cadence is one-tenth of the total timeout, minimum one second — so this call wakes every 30 seconds.

  2. Both COMPLETED and FAILED are terminal states from the caller’s perspective. Supplying only COMPLETED would cause the method to return false on a failed run even though the job has definitively stopped.

Returning false from monitor.await means the timeout expired before any of the requested statuses was observed; the job may still be running. Returning true means one of the supplied statuses was observed — inspect JobExecution.getExitStatus() if you need to distinguish COMPLETED from FAILED.

monitorLatest(logicalName) resolves the most recent JobInstance for a named job, then the most recent JobExecution against that instance, and returns a monitor for it.

import java.util.concurrent.TimeUnit;
import org.smallmind.batch.spring.BatchJobFactory;
import org.smallmind.batch.spring.BatchJobMonitor;
import org.springframework.batch.core.ExitStatus;

public class StatusProbe {

  public void waitForNightlyJob (BatchJobFactory factory)
    throws Exception {

    BatchJobMonitor monitor = factory.monitorLatest("nightlyReconciliation");

    if (!monitor.await(10, TimeUnit.MINUTES, ExitStatus.COMPLETED)) {
      throw new IllegalStateException("nightly reconciliation did not finish in time");
    }
  }
}

If no instance has ever existed for the name — typically because the job bean is declared but has never been launched — monitorLatest throws FormattedNoSuchJobException rather than silently returning true.

BatchJobWatcher is the right tool for a coordinated shutdown: wait until everything the repository knows about is done, and has been done for a little while.

import java.util.concurrent.TimeUnit;
import org.smallmind.batch.spring.BatchJobFactory;
import org.smallmind.batch.spring.BatchJobWatcher;

public class ShutdownGate {

  public void drain (BatchJobFactory factory)
    throws InterruptedException {

    BatchJobWatcher watcher = factory.watch();

    if (!watcher.await(5, 120, TimeUnit.SECONDS)) {                      (1)
      throw new IllegalStateException("batch activity did not quiesce");
    }
  }
}
  1. clear = 5 seconds, timeout = 120 seconds: every visible execution must be in COMPLETED status, continuously, for at least five seconds before the watcher returns true. The five-second floor is what keeps the watcher from declaring success during the gap between one execution finishing and the next being scheduled.

A deployment script, a blue/green cutover controller, or a database-side maintenance gate may not have a JobRepository handy, but it does have the BATCH_* tables. BatchJobInterrogator polls them with a single SELECT COUNT(*) … WHERE STATUS != 'COMPLETED' per cycle.

import java.util.concurrent.TimeUnit;
import org.smallmind.batch.spring.BatchJobInterrogator;

public class ExternalGate {

  public boolean waitForBatchIdle (BatchJobInterrogator interrogator)
    throws Exception {

    return interrogator.awaitQuiescence(10, TimeUnit.MINUTES);
  }
}

Wire the interrogator as a regular Spring bean when you want it inside a context:

<bean id="batchInterrogator" class="org.smallmind.batch.spring.BatchJobInterrogator">
  <property name="dataSource" ref="batchDataSource"/>
  <property name="schemaName" value="batch"/>
</bean>

The schemaName property is concatenated into the SQL unqualified; pick a value you trust.

JobFactory.restart(executionId) delegates to JobOperator.restart(JobExecution), which re-runs the execution with the same parameters. If the execution id cannot be found, a FormattedJobRestartException (a NoSuchJobException subclass) is raised with a formatted message identifying the id that was searched for.

public class RestartController {

  private final JobFactory jobFactory;

  public RestartController (JobFactory jobFactory) {

    this.jobFactory = jobFactory;
  }

  public void retry (long previousExecutionId)
    throws Exception {

    jobFactory.restart(previousExecutionId);
  }
}

The restart uses the identifying parameters recorded in BATCH_JOB_EXECUTION_PARAMS for the original execution, so it targets the same job instance. Spring Batch decides from the instance’s state whether the restart is permitted; a completed instance cannot be restarted and will throw JobInstanceAlreadyCompleteException out of the underlying operator.

Tasklet bodies read parameters off the ChunkContext. TaskletUtility.asBoolean is a one-liner convenience for the common case of a boolean flag passed as a string parameter (Spring Batch does not have a native boolean parameter type, so booleans are usually stored as "true" / "false" strings):

import org.smallmind.batch.spring.TaskletUtility;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;

public class ReconciliationTasklet implements Tasklet {

  @Override
  public RepeatStatus execute (StepContribution contribution, ChunkContext chunkContext) {

    boolean dryRun = TaskletUtility.asBoolean(chunkContext, "dryRun", false);

    if (dryRun) {
      // report what would happen, without mutating state
    } else {
      // perform the reconciliation
    }

    return RepeatStatus.FINISHED;
  }
}

For non-boolean types, read directly from chunkContext.getStepContext().getJobParameters(); the map’s values are already the correct Java type because JobParametersBuilder routed them through the matching addXxx call at launch time.

The default concurrencyLimit on BatchJobExecutorFactory is 1, matching the batch.xml wiring. A service that wants to run a small number of jobs in parallel overrides the bean:

<bean id="jobOperator" class="org.springframework.batch.core.launch.support.SimpleJobOperator">
  <property name="jobRepository" ref="jobRepository"/>
  <property name="taskExecutor">
    <bean class="org.smallmind.batch.spring.BatchJobExecutorFactory">
      <property name="concurrencyLimit" value="4"/>
    </bean>
  </property>
</bean>

Values below 1 are silently clamped to 1 — an empty pool would simply swallow every submission and stall the operator, which is never the desired outcome.

Spring Batch decides which instance a launch targets by hashing the identifying subset of the supplied parameters. BatchParameter.isIdentifying() is the only knob that controls this.

  • Making a parameter identifying ties it to the instance key. Two launches with the same identifying parameters will collide on the second attempt against an active or completed instance.

  • Making a parameter non-identifying decouples it from the key while still delivering it to the tasklet. Use this for inputs that influence behavior but should not create a new instance (e.g., a per-run correlation id, a dryRun flag, or an audit trail note).

At least one identifying parameter — or a parameter set that is obviously unique per run (a timestamp, a generation counter) — is essential for repeatable jobs: otherwise, the second launch gets JobInstanceAlreadyComplete on the first re-attempt.

All three waiting helpers (BatchJobMonitor.await, BatchJobWatcher.await, BatchJobInterrogator.awaitQuiescence) share the same cadence rule:

  • Poll interval is max(1000ms, timeout / 10).

  • The final sleep before the deadline is shortened to remaining so the call returns promptly when the timeout is about to expire.

The minimum-one-second floor is deliberate: it keeps these helpers from hammering the database with sub-second queries when the caller supplies a short total timeout.

The Spring Batch distinction is preserved and matters.

  • BatchStatus describes where an execution sits in the lifecycle: STARTING, STARTED, STOPPING, STOPPED, COMPLETED, FAILED, ABANDONED, UNKNOWN. BatchJobWatcher and BatchJobInterrogator key off COMPLETED in this set.

  • ExitStatus is the higher-level outcome tag: COMPLETED, FAILED, STOPPED, NOOP, EXECUTING, plus any custom code the tasklet or step returns. BatchJobMonitor.await matches against this set.

A job with BatchStatus.COMPLETED has stopped; its ExitStatus carries the outcome interpretation. Match on whichever dimension your caller actually cares about.

BatchJobFactory.create logs one line per launch at INFO through scribe-pen:

Batch(nightlyReconciliation) for reason(scheduled nightly reconciliation) with parameters({runDate: [type=DATE,value=2026-04-21T03:00,identifying=true],dryRun: [type=STRING,value=false,identifying=false]})

This is the primary audit trail for launches. A null reason is rendered as the literal string "unknown"; an empty or null parameter map as "none". If a tighter audit record is needed, wrap the factory and emit whatever you like before delegating.

  • BatchJobFactory is safe for concurrent create, restart, monitor, monitorLatest, and watch calls from multiple threads — it delegates to the underlying repository and operator, both of which are designed for multi-threaded use.

  • BatchJobMonitor, BatchJobWatcher, and BatchJobInterrogator are single-use helpers. Each call to await is independent; there is no shared mutable state across calls and no cleanup required on the caller.

  • BatchJobRegistry is populated once during postProcessBeanFactory and thereafter read-only. The underlying HashSet is never mutated after context startup, so concurrent getJobNames and getJob calls are safe.

  • BatchJobExecutorFactory is a singleton FactoryBean. The TaskExecutorAdapter it vends wraps a ScheduledThreadPoolExecutor, which is itself thread-safe.

The exceptions callers will see come in three rough groups.

Pre-launch failures

Raised by BatchJobFactory.create when the job name cannot be resolved (FormattedNoSuchJobException) or the parameter set fails Spring Batch’s validation (InvalidJobParametersException).

Launch-state failures

JobExecutionAlreadyRunningException (an active execution against the same instance already exists), JobInstanceAlreadyCompleteException (the parameters target a completed instance; supply a distinguishing identifying parameter), and JobRestartException (the instance is in a state Spring Batch considers non-restartable). All propagate out of the underlying JobOperator.

Restart lookup failures

BatchJobFactory.restart(executionId) throws FormattedJobRestartException (a NoSuchJobException) when no execution with that id can be located. Lookup of the latest execution by logical name throws FormattedNoSuchJobException with the same parent type.

Both formatted exceptions exist purely so that the diagnostic (the logical name or the execution id) can be embedded in the message without a separate String.format call at every throw site.

Setting Default Where set

BatchJobExecutorFactory.concurrencyLimit

1 (clamped minimum)

BatchJobExecutorFactory

JobRepositoryFactoryBean.databaseType

mysql

batch.xml

JobRepositoryFactoryBean.maxVarCharLength

2500

batch.xml

JobRepositoryFactoryBean.validateTransactionState

false

batch.xml

JobRepositoryFactoryBean.isolationLevelForCreate

REPEATABLE_READ

batch.xml

Poll floor (all await methods)

1000 ms

Math.max(1000, total/10)

asBoolean absent-parameter result

caller’s defaultValue

TaskletUtility

create/restart reason when null

literal string "unknown" in the log

BatchJobFactory.create

Batch.changelog.xml creates the standard Spring Batch metadata schema as of Spring Batch 5 (the JSR-352 artifacts are not included):

  • BATCH_JOB_INSTANCE — one row per distinct (job name, identifying parameter set).

  • BATCH_JOB_EXECUTION — one row per launch or restart attempt.

  • BATCH_JOB_EXECUTION_PARAMS — the parameter values recorded at launch. Each row has a PARAMETER_TYPE column holding one of Spring Batch’s type names (STRING, LONG, DOUBLE, LOCAL_DATE_TIME, …), which is why ParameterType has exactly the four constants it does.

  • BATCH_JOB_EXECUTION_CONTEXT / BATCH_STEP_EXECUTION_CONTEXT — the serialized execution context (job-scoped and step-scoped respectively).

  • BATCH_STEP_EXECUTION — one row per step attempt within an execution.

  • BATCH_JOB_INSTANCE_SEQ / BATCH_JOB_EXECUTION_SEQ / BATCH_STEP_EXECUTION_SEQ — the three single-row tables Spring Batch uses as sequences on engines without native sequence support.

  • Foreign keys, the JOB_INST_UN unique constraint on (JOB_NAME, JOB_KEY), and a set of non-unique indexes on the most commonly filtered columns.

The changelog is run on the batchDataSource. If your application already manages its own Liquibase runner, you can pull just the changelog file in yourself:

<bean id="applicationLiquibase" class="org.smallmind.liquibase.spring.SpringLiquibase">
  <property name="dataSource" ref="myDataSource"/>
  <property name="source" value="CLASSPATH"/>
  <property name="goal" value="UPDATE"/>
  <property name="changeLogs">
    <list>
      <!-- application-specific changelogs here -->
      <bean class="org.smallmind.liquibase.spring.ChangeLog">
        <property name="input" value="org/smallmind/batch/spring/Batch.changelog.xml"/>
        <property name="output" value="Batch.changelog.xml"/>
      </bean>
    </list>
  </property>
</bean>

In that case, omit batch-liquibase.xml from your imports and define your own batchDataSource bean directly.

NoSuchJobException at launch time

BatchJobRegistry did not discover a bean assignable to Job under the name you passed. Check: (a) that the job bean id exactly matches the logical name, (b) that BatchJobRegistry is present in the context (it is wired automatically via batch.xml), and (c) that the bean is declared before the context is refreshed — the registry scans during postProcessBeanFactory, not lazily.

JobInstanceAlreadyCompleteException on every second launch

None of your parameters were identifying, so every launch hashes to the same instance key. Mark at least one parameter identifying, typically a timestamp or a correlation id.

BatchJobWatcher.await returns false despite everything looking idle

Either the timeout was shorter than the clear period, or an execution just finished inside the clear window. The clear period resets whenever hasCompleted() observes any non-COMPLETED execution; immediately after a new run starts, the clock starts over.

Liquibase reports "table already exists" on startup

Another component created the BATCH_* tables independently. Either remove the duplicate schema creation path or set the batch-liquibase.xml changelog’s MODE property (through `smallmind-liquibase’s own configuration knobs) so Liquibase can synchronise rather than re-create.

BatchJobInterrogator returns false but no jobs are visible in the UI

The interrogator reads by STATUS != 'COMPLETED', which includes FAILED, STOPPED, ABANDONED, and UNKNOWN. A long-dead failed execution keeps the interrogator unhappy until it is either restarted to COMPLETED or administratively removed. This is intentional — a failed execution that nobody has decided how to handle is not idle.

Single-node coordination

BatchJobWatcher and BatchJobInterrogator only see executions that live in the repository they query. In a multi-node cluster where each node has its own operator, a watcher on node A cannot detect activity submitted on node B unless both share the same JobRepository schema.

No boolean parameter type

Spring Batch does not have a native boolean parameter; booleans travel as strings and are parsed on the tasklet side. BatchParameters mirrors this: there is no putBoolean. Use putString(key, "true", …) and TaskletUtility.asBoolean at the read site.

No manual job registration

BatchJobRegistry.register and BatchJobRegistry.unregister throw UnsupportedOperationException. If you need dynamic job construction, expose the constructed Job as a bean before context refresh or run a second context with the additional beans included; the registry does not support mutation after startup.

BatchJobInterrogator reads counts, not structure

It answers "are there any non-completed executions?" and nothing else. A caller that wants a richer view (which jobs, which steps, which parameters) should query the BATCH_* tables directly or use a JobRepository.

smallmind-batch makes Spring Batch approachable: a neutral parameter envelope that keeps upstream callers off the Spring Batch API, a ready-to-import XML composition root that wires the five beans every Spring Batch deployment needs, a registry that discovers jobs automatically, and three polling helpers that cover the waiting patterns a service actually uses. batch-base is the contract edge (JobFactory, BatchParameters); batch-spring is the runtime. Import batch.xml, declare your jobs as beans, and launch them through jobFactory.