Skip to content

utlibraries/pid-handle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Handle Service (Django) — README

This repository provides an authenticated Django REST API for managing Handle.Net persistent identifiers (PIDs). The API enqueues operations and processes them asynchronously in a background worker that talks to the Handle server via Java CLI tools.

Key principles

  • Async by default: POST/DELETE mutate operations return 202 Accepted with a request id. Results are retrieved by polling /handle-ops/{id}.
  • Pattern A (write-on-success): The local Handle table is updated only after the Handle server confirms success. No local “pending/error” mirror.
  • One in-flight op per handle: The API rejects a new op for a handle when another is queued/running (HTTP 423 Locked).
  • Strict semantics:
    • CREATE fails if a local handle already exists (active or deleted). Handles are never recycled.
    • UPDATE fails if the handle is missing or deleted.
    • DELETE fails if the handle is missing or already deleted.
  • No idempotency header: Idempotency-Key is not used in this iteration.

Table of Contents

  1. Architecture
  2. Data Model
  3. API
  4. Background Worker
  5. Configuration
  6. Local Development
  7. Admin
  8. Deploy & Operations
  9. Migrations & Breaking Changes
  10. Changelog (2025-09)

Architecture

Client
  │  HTTP (Token)
  ▼
Django REST API  ──(enqueue)──► Postgres (requests, confirmed handles)
  │                                   ▲
  │                                   │
  └──poll /handle-ops/{id}◄───────────┘
       │
       ▼
Background Worker ──► Java CLI ──► Handle.Net server (authoritative)
                     (subprocess)
  • Source of truth for handle values is the Handle server.
  • Django stores:
    • HandleOperationRequest rows for in-flight / logs / outcomes.
    • Handle rows as a confirmed local mirror (only after success).

Data Model

Handle

  • Fields: handle (suffix, unique), full_handle (prefix/suffix, unique), current_url, version, status, last_request, timestamps.
  • status: active or deleted only.
    • No pending/error here—those states exist only on requests.
  • Deletion never removes rows; status flips to deleted to prevent suffix recycling.
  • last_request references the last successful HandleOperationRequest.

HandleOperationRequest

  • Fields: op (create|update|delete|remote_resolve|remote_list), status (queued|running|success|error), handle, full_handle, url (for create/update), attempts/timings, and logs.
  • CheckConstraint: url must be present when op in ('create','update').
  • Note: idempotency_key has been removed.

API

Authentication

All endpoints require DRF token auth:

python manage.py drf_create_token <username>
# Use header: Authorization: Token <token>

Endpoints

Method Path Purpose Notes
GET /handles/ List local handles (Postgres mirror; non-authoritative) Sync
POST /handles/ Enqueue CREATE for a new handle 202 + request id
GET /handles/{handle_id}/ Local resolve (read the Handle row) 404 if not found
POST /handles/{handle_id}/ Enqueue UPDATE of target URL 202 + request id
DELETE /handles/{handle_id}/ Enqueue DELETE (mark local row deleted on success) 202 + request id
GET /handles/remote/{handle_id}/ Enqueue remote resolve against Handle.Net (authoritative) 202 + request id; result in java_stdout
GET /handles/remote/ Enqueue remote list for the configured prefix 202 + request id; one handle per line in java_stdout
GET /handle-ops/{id} Get request status/logs (queued/running/success/error) Sync
GET /health/ (if enabled) Health check Sync

Async pattern: mutate/remote endpoints return 202 Accepted and a HandleOperationRequest summary; clients poll /handle-ops/{id} to read status and logs (java_stdout, java_stderr, error).

Response Shapes

Handle read

{
  "id": 1,
  "handle": "example123",
  "full_handle": "20.500.14725/example123",
  "version": 2,
  "current_url": "https://repo.org/item/123",
  "status": "active",
  "last_request": 42,
  "last_synced_at": "2025-09-01T12:34:56Z",
  "created_at": "2025-08-26T23:45:00Z",
  "updated_at": "2025-09-01T12:34:56Z"
}

HandleOperationRequest summary

{
  "id": 42,
  "op": "update",
  "handle": "example123",
  "full_handle": "20.500.14725/example123",
  "url": "https://new.example.org/item/123",
  "status": "queued",
  "attempts": 0,
  "started_at": null,
  "finished_at": null,
  "created_at": "2025-09-01T12:30:00Z"
}

Error Semantics

  • 423 Locked — returned synchronously if another request for the same full_handle is queued or running. (One in-flight op per handle.)
  • 404 Not Found — for /handles/{handle_id}/ reads when the local mirror is missing.
  • Asynchronous validation failures (strict rules below) result in the request eventually reaching status="error", visible via /handle-ops/{id}. Examples:
    • CREATE for an existing local handle (active or deleted)
    • UPDATE for a missing/deleted handle
    • DELETE for a missing/already-deleted handle

The API intentionally accepts these operations (202) and enforces strict preflights in the worker to keep the API thin and the system consistent.


Background Worker

  • Runs a loop with a ThreadPoolExecutor (HANDLE_WORKER_THREADS).
  • Claim shaping: claims at most one queued request per full_handle per tick.
  • Per-handle lock: uses Postgres advisory locks to ensure only one job per handle executes concurrently (even across processes).
  • Pattern A write-back: only mutates the Handle row on success:
    • CREATE success → insert new row (active, version=1).
    • UPDATE success → require row exists & not deleted; bump version, set URL, active.
    • DELETE success → require row exists; set deleted (never remove the row).
  • Strict preflights (before calling the Handle server):
    • CREATE: error if row exists (active/deleted).
    • UPDATE: error if row missing or deleted.
    • DELETE: error if row missing or already deleted.
  • Reaper: flips long-running running requests to error after HANDLE_WORKER_CLAIM_TIMEOUT seconds to free stuck lanes.
  • Remote ops: remote_resolve / remote_list do not mutate Handle; their output appears in java_stdout for clients to parse.

Configuration

Environment variables (with defaults):

Variable Purpose Default
HANDLE_PREFIX Your handle prefix, e.g. 20.500.14725 required
HANDLE_WORKER_POLL_INTERVAL Worker loop sleep in seconds 2.0
HANDLE_WORKER_BATCH Max jobs to claim per tick 5
HANDLE_WORKER_THREADS Worker thread pool size 2
HANDLE_WORKER_LOCK_ID Leader election advisory lock id 726381920
HANDLE_WORKER_CLAIM_TIMEOUT Seconds before a running job is reaped as error 900
HANDLE_WORKER_BATCH_SCAN_FACTOR Oversampling factor to pick distinct handles per tick 4

Concurrency tuning: With THREADS=N, at most N different handles will execute concurrently. Additional jobs remain queued.


Local Development

Prereqs

  • Python 3.11+ (or containerized environment)
  • Postgres
  • Java (for the Handle CLI) if running the worker directly

Setup

docker compose up

python manage.py migrate

# Create a superuser and token
python manage.py createsuperuser
python manage.py drf_create_token <username>

Quick usage

# Enqueue a create
curl -X POST http://localhost:8000/handles/   -H "Authorization: Token $TOKEN"   -H "Content-Type: application/json"   -d '{"handle":"example123","url":"https://repo.org/item/123"}'

# Poll the request
curl -H "Authorization: Token $TOKEN" http://localhost:8000/handle-ops/42

# Read the local mirror
curl -H "Authorization: Token $TOKEN" http://localhost:8000/handles/example123/

Admin

Django Admin provides convenient browsing of both models:

  • HandleOperationRequest: filter by op, status, timings; view java_stdout/stderr/error.
  • Handle: see status (active/deleted), version, current_url, and link to the last_request that changed it.

Deploy & Operations

  • The worker is embedded and starts once per app instance using a leader election advisory lock (only one worker loop per DB).
  • Monitor logs for:
    • Reaped X stuck RUNNING handle ops
    • CREATE/UPDATE/DELETE preflight ... (worker rejections)
  • Backups: persist both the Handle server data volume (BerkeleyDB) and the Postgres database.

Migrations & Breaking Changes

  • Removed: HandleOperationRequest.idempotency_key.
  • Changed: Handle.status is now only active or deleted.
  • Added: CheckConstraint requiring url for create & update requests.
  • Indexes: ('full_handle','status'), created_at indices added (see models).

If you have legacy Handle rows with pending/error statuses, normalize them (e.g., to active) via a short data migration to align with Pattern A.


Changelog (2025-09)

  • Pattern A: local Handle rows are only written on successful upstream operations.
  • Strict worker preflights for create/update/delete.
  • Per-handle concurrency enforced at API (423) and worker (advisory locks).
  • Removed idempotency (no Idempotency-Key header).
  • Schema updates: status vocabulary reduced; URL presence constraint; new indexes.
  • Docs: clarified async semantics (202 + poll), error behavior, and configuration knobs.

© 2025 — Internal service for handle management.

About

Handle.net Server + Django API for persistent identifier registration and management

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors