Models, migrations, and jobs¶
A plugin owns its own Django app, models, migrations, and background-job identifiers. It may depend on Core and its own migration history, never on a sibling plugin's migrations or private models.
Model and migration boundary¶
Put persistent data in a plugin Django app named in PluginDescriptor.django_apps.
Reference Core through public contracts or stable identifiers; do not make a
migration dependency on another plugin. Prefer additive, expand-contract
changes. Add a nullable field or table, deploy code that tolerates both shapes,
backfill separately, then make a contracted change only after reviewing its
operational impact.
Run the repository checks before proposing a destructive operation:
The latter reports the app, migration, and conflicting plugin ownership. A
suppression for the migration safety linter needs an explicit, reviewed
# safe-migrations: ignore ... comment. It does not replace a backup or
deployment plan. See data safety.
Register scheduled work¶
Atlas Core owns one scheduler process for the whole distribution: the
runapscheduler management command (the ingestor service in the Compose
files), backed by the django-apscheduler job store. A plugin does not run its
own scheduler. It contributes jobs in two steps.
- Declare every job id on the descriptor.
- Expose a module-level
register_jobs(scheduler)hook on the entry-point module (the module named byentry_point, next toregister_runtime()).
PLUGIN = PluginDescriptor(
id="atlas.inventory",
version="0.1.0",
compatibility={"atlasCore": ">=0.1 <1"},
django_apps=("atlas_plugin_inventory",),
entry_point="atlas_plugin_inventory.plugin:PLUGIN",
job_ids=("atlas.inventory.refresh",),
)
def register_jobs(scheduler) -> None:
from atlas_plugin_api import add_interval_job
from .jobs import refresh_inventory
add_interval_job(
scheduler, refresh_inventory, job_id="atlas.inventory.refresh", seconds=300
)
When the scheduler starts, Core calls the hook once for every selected plugin
that is not disabled. A plugin without the hook is skipped, and a disabled
plugin's hook is not called. Importing the plugin module must not register or
start anything; keep imports of job bodies and django_apscheduler inside the
hook so a distribution without the plugin never imports them.
add_interval_job applies the platform defaults: the job body is wrapped in
close_old_connections, only one instance runs at a time (an overlapping
trigger is skipped), and a persisted job with the same id is replaced. For a
different trigger, call scheduler.add_job directly; the id must still be
declared on the descriptor.
If the hook registers an id the descriptor does not declare, scheduler startup fails and names the plugin and the id. This keeps pause and resume by id trustworthy: Atlas pauses declared jobs while the plugin is disabled and resumes them when it is active, by writing to the job store, so it works regardless of which process changes the state. Startup tolerates a job that is not yet present in a fresh job store.
Run exactly one scheduler process per deployment. The job store has no leader
election, so a second runapscheduler would run every job twice. The
scheduler also starts, with no jobs, in a distribution where no selected plugin
contributes any.
Jobs must be idempotent, tolerate an unavailable dependency, and avoid writing data after their plugin is disabled. A job that raises is logged and does not stop other jobs. Treat disablement and removal as data-preserving states; only an operator's explicit purge deletes scoped model rows. See plugin lifecycle.
Run a plugin's work as a separate deployment¶
The shared scheduler is the default, not a requirement. A plugin may also ship
a management command that performs its work once, built on the same job
function the hook registers, so there is one implementation. Ingestion does
this with manage.py ingest. An operator can run such a command from cron, a
Kubernetes CronJob, or a dedicated container, with or without
runapscheduler running. If both run, the job body must tolerate overlapping
executions.
Verify lifecycle safety¶
Test model behavior, migration paths, job registration, failure isolation, and disabled-job pause/resume. Do not put a sibling plugin's ORM access in a model method or migration. Continue with testing plugins and backend collaboration.