Class IP2LocationManager

java.lang.Object
co.mindus.ip2location.IP2LocationManager

public final class IP2LocationManager extends Object
Singleton manager for IP2Location services with hot-swap reconfiguration support.

This class provides a global access point to IP geolocation services while supporting live reconfiguration without service interruption. It ensures there is always an active instance available to serve requests, even during configuration changes.

Key Features

  • Always-On Service: Lookups never fail due to reconfiguration
  • Atomic Handoff: New instance becomes active only when fully ready
  • Graceful Shutdown: Old instance completes in-flight requests before disposal
  • File Reuse: Downloaded databases are reused when directories match
  • Cleanup Support: Old configuration files can be cleaned up safely
  • Async Support: Non-blocking configuration with callbacks
  • Status Listeners: Subscribe to service events via IServiceStatusListener
  • Human-readable Status: getStatusDescription() for UI display

Reconfiguration Flow

┌─────────────────────────────────────────────────────────────────────────┐
│ 1. User calls reconfigure() or reconfigureAsync() with new config       │
│ 2. Manager creates new IP2LocationService instance                      │
│ 3. New instance initializes (loads/downloads databases)                 │
│ 4. Manager waits for new instance to become ready                       │
│ 5. Manager atomically swaps the active reference                        │
│ 6. Old instance enters "draining" state:                                │
│     └─ Completes in-flight requests                                     │
│     └─ Rejects new requests (routed to new instance)                    │
│ 7. After grace period (30s default), old instance is disposed           │
│ 8. Optional: cleanupOldConfiguration() removes orphaned files           │
└─────────────────────────────────────────────────────────────────────────┘

Usage Example - Basic

// Get singleton instance
IP2LocationManager manager = IP2LocationManager.getInstance();

// Initial configuration (blocking)
manager.configure(new IP2LocationService.Builder()
    .downloadToken("your-token")
    .database(Database.DB11)
    .directory(Paths.get("/var/lib/ip2location"))
);

// Perform lookups (thread-safe, from any thread)
GeoLocation loc = manager.lookup("8.8.8.8");
if (loc.isOk()) {
    System.out.println("Country: " + loc.getCountryCode());
    System.out.println("Location: " + loc.getLocationDescription());
}

// Display status in UI
statusLabel.setText(manager.getStatusDescription());
// "IPv4+IPv6, IPv4 ready, IPv6 ready"

// On application shutdown
manager.shutdown();

Usage Example - Async with Virtual Threads

IP2LocationManager manager = IP2LocationManager.getInstance();

// Configure with virtual threads (Java 21+)
manager.configureAsync(
    new IP2LocationService.Builder()
        .downloadToken("your-token")
        .threadFactory((task, name, daemon) -> 
            Thread.ofVirtual().name(name).unstarted(task))
        .database(Database.DB11),
    (service, success, error) -> {
        if (success) {
            System.out.println("Service ready!");
        }
    }
);

// Wait for service to be ready before accepting traffic
manager.awaitReady(60, TimeUnit.SECONDS);

Usage Example - Hot-Swap Reconfiguration

// Later: reconfigure without service interruption
manager.reconfigure(new IP2LocationService.Builder()
    .downloadToken("new-token")
    .database(Database.DB5)             // Different DB type
    .includeIPv6(true)                   // Enable IPv6
    .directory(Paths.get("/var/lib/ip2location-v2"))  // New directory
);

// Clean up files from old configuration
manager.cleanupOldConfiguration();

Usage Example - Status Listeners

// Listen for status changes
manager.addStatusListener(new IServiceStatusListener() {
    @Override
    public void onStatusChanged(String newStatus) {
        SwingUtilities.invokeLater(() -> statusLabel.setText(newStatus));
    }

    @Override
    public void onDatabaseUpdated(boolean ipv4, boolean ipv6) {
        System.out.println("Databases updated: IPv4=" + ipv4 + ", IPv6=" + ipv6);
    }
});

// Check if update can be performed
if (manager.canUpdateDatabasesNow()) {
    manager.updateDatabasesNow();
}

Cluster Support

In a clustered server environment, multiple JVM instances of the host server run concurrently. Each JVM has its own IP2LocationManager singleton. The host's bootstrap sets the cluster index via setClusterIndex(int) and gives each instance its own database directory by appending "#" + index to the configured path (e.g. IP2Location#1/, IP2Location#2/).

Key cluster behaviors:

  • Each JVM operates fully independently — own downloads, state file, scheduler, recovery, and event log.
  • All instances share the same host configuration. Only the database path differs per instance.
  • Configuration changes should be made through the first cluster server's administration UI (index=1). Non-primary instances (index≥2) should display settings as read-only. Use getClusterIndex() and isClusterInstance() to determine the appropriate UI behavior.
  • After configuration changes, the host restarts the other instances to pick up the new settings.

Thread Safety

All methods are thread-safe. Lookups use read locks that allow high concurrency, while reconfiguration uses write locks to ensure atomic handoff. In-flight requests during reconfiguration will complete successfully using whichever instance was active when the request started.

File Reuse Strategy

When reconfiguring:

  • If the new directory is the same as the old, database files are reused automatically
  • If directories differ, new instance downloads fresh databases
  • State files (.ip2location-state.properties) are per-directory and not shared
  • Old directory's files can be cleaned up with cleanupOldConfiguration()

Request Tracking

The manager tracks in-flight requests to each service instance. During reconfiguration, the old instance is kept alive until all its in-flight requests complete (or timeout).

Author:
Christopher Mindus
See Also:
  • Method Details

    • getInstance

      public static IP2LocationManager getInstance()
      Returns the singleton manager instance.

      This method is thread-safe and returns the same instance for all callers.

      Returns:
      The global IP2LocationManager instance
    • configure

      public void configure(IP2LocationService.Builder builder)
      Configures the manager with an initial service instance (blocking).

      This method blocks until the service is fully initialized and ready for lookups. For non-blocking initialization, use configureAsync(IP2LocationService.Builder, IInitializationCallback).

      If a service is already configured, this method behaves like reconfigure(IP2LocationService.Builder).

      Parameters:
      builder - The builder with configuration settings
      Throws:
      IllegalStateException - if the manager has been shut down
      IllegalArgumentException - if builder is null
    • configureAsync

      public void configureAsync(IP2LocationService.Builder builder, IInitializationCallback callback)
      Configures the manager with an initial service instance (non-blocking).

      Returns immediately. The service initializes in the background. Use awaitReady(long, TimeUnit) to wait for initialization, or provide a callback to be notified when complete.

      Parameters:
      builder - The builder with configuration settings
      callback - Optional callback for initialization result (may be null)
      Throws:
      IllegalStateException - if the manager has been shut down
      IllegalArgumentException - if builder is null
    • reconfigure

      public void reconfigure(IP2LocationService.Builder builder)
      Reconfigures the manager with a new service instance (blocking).

      This method performs a hot-swap of the service:

      1. Creates and initializes the new service (blocking)
      2. Atomically swaps the active reference
      3. Drains the old instance (completes in-flight requests)
      4. Disposes the old instance after drain completes

      Lookups continue to work throughout this process without interruption.

      If the new configuration uses the same directory as the old one, database files are automatically reused. The new instance will detect existing files and only download if they're outdated.

      Parameters:
      builder - The builder with new configuration settings
      Throws:
      IllegalStateException - if manager is shut down
      IllegalArgumentException - if builder is null
    • reconfigureAsync

      public void reconfigureAsync(IP2LocationService.Builder builder, IInitializationCallback callback)
      Reconfigures the manager with a new service instance (non-blocking).

      Returns immediately. The new service initializes in the background. The swap occurs only after the new instance is ready.

      Parameters:
      builder - The builder with new configuration settings
      callback - Optional callback for reconfiguration result (may be null)
      Throws:
      IllegalStateException - if manager is shut down
      IllegalArgumentException - if builder is null
    • lookup

      public GeoLocation lookup(String ip)
      Looks up geolocation information for an IP address.

      This method is thread-safe and can be called concurrently from multiple threads. It automatically uses the currently active service instance.

      During reconfiguration, lookups continue to work without interruption. Requests that started before the swap will complete using the old instance, while new requests use the new instance.

      Parameters:
      ip - The IP address to look up (IPv4 or IPv6)
      Returns:
      A GeoLocation containing the result
      See Also:
    • awaitReady

      public boolean awaitReady(long timeout, TimeUnit unit) throws InterruptedException
      Waits for the manager to have a ready service.

      Blocks until a service is configured and ready, or timeout expires.

      Parameters:
      timeout - The maximum time to wait
      unit - The time unit
      Returns:
      true if ready, false if timeout expired
      Throws:
      InterruptedException - if interrupted while waiting
    • isReady

      public boolean isReady()
      Returns whether the manager has a ready service.
      Returns:
      true if a service is configured and ready for lookups
    • cleanupOldConfiguration

      public boolean cleanupOldConfiguration()
      Cleans up files from the old configuration directory.

      This method should be called after reconfiguration when you want to remove database files from the previous configuration's directory. It only has effect if the new configuration uses a different directory than the old one.

      Files cleaned up:

      • *.BIN files (IP2Location databases)
      • .ip2location-state.properties (download state)
      • *.tmp files (incomplete downloads)

      The directory itself is removed if empty after cleanup.

      Returns:
      true if cleanup was performed, false if nothing to clean up
    • getOldConfigurationDirectory

      public Path getOldConfigurationDirectory()
      Returns the path to the old configuration directory awaiting cleanup.
      Returns:
      Path to old directory, or null if none
    • isConfigured

      public boolean isConfigured()
      Returns whether the manager has an active service configured.
      Returns:
      true if a service is configured (may still be initializing)
    • isEnabled

      public boolean isEnabled()
      Returns whether the service is enabled.

      When the service is disabled, lookups return GeoLocation.Status.SERVICE_DISABLED.

      Returns:
      true if the service is enabled, false if disabled or not configured
    • isIPv4Ready

      public boolean isIPv4Ready()
      Returns whether IPv4 lookups are available.
      Returns:
      true if IPv4 database is loaded and ready
    • isIPv6Ready

      public boolean isIPv6Ready()
      Returns whether IPv6 lookups are available.
      Returns:
      true if IPv6 database is loaded and ready
    • getLastUpdateMonth

      public YearMonth getLastUpdateMonth()
      Returns the month of the last successful database update.
      Returns:
      YearMonth of last update, or null if never updated
    • isReconfiguring

      public boolean isReconfiguring()
      Returns whether a reconfiguration is currently in progress.

      A reconfiguration is in progress if an old instance is still draining.

      Returns:
      true if an old instance is still draining
    • getActiveRequestCount

      public int getActiveRequestCount()
      Returns the current active request count.

      This is the number of lookup requests currently being processed by the active instance.

      Returns:
      Number of requests currently being processed
    • isShutdown

      public boolean isShutdown()
      Returns whether the manager has been shut down.
      Returns:
      true if shutdown() has been called
    • setClusterIndex

      public void setClusterIndex(int index)
      Sets the cluster index for this server instance.

      This method is called by the host's bootstrap during server startup. It should not be called from application code.

      The cluster index determines the operating mode:

      • 0 — Normal (unclustered) server. All configuration and administration features are available.
      • 1 — First (primary) cluster server. Configuration changes should be made through this instance's administration UI. After changes, the host restarts other instances to apply them.
      • ≥2 — Non-primary cluster server. The administration UI should restrict configuration editing on these instances, displaying settings as read-only.
      Parameters:
      index - The cluster index: 0 for unclustered, ≥1 for clustered instances
      See Also:
    • getClusterIndex

      public int getClusterIndex()
      Returns the cluster index of this server instance.

      The cluster index identifies this JVM's role within a cluster of host server instances:

      • 0 — Normal (unclustered) server
      • 1 — First (primary) cluster server
      • ≥2 — Non-primary cluster server

      Use isClusterInstance() for a simpler check of whether this is a clustered server.

      Returns:
      The cluster index (0 = unclustered, ≥1 = cluster instance)
      See Also:
    • isClusterInstance

      public boolean isClusterInstance()
      Returns whether this server is running as a cluster instance.

      A cluster instance runs in a separate JVM managed by the host server's cluster controller. Each instance has its own database directory (with a #N suffix) and operates independently for downloads, lookups, and recovery.

      Equivalent to getClusterIndex() > 0.

      Returns:
      true if this is a clustered server instance
      See Also:
    • getIPv4DatabaseDate

      public LocalDate getIPv4DatabaseDate()
      Returns the date of the currently loaded IPv4 database.

      The date reflects when the database was published by IP2Location, typically the 1st of the month.

      Returns:
      The database date, or null if no IPv4 database is loaded or no service is configured
    • getIPv6DatabaseDate

      public LocalDate getIPv6DatabaseDate()
      Returns the date of the currently loaded IPv6 database.

      The date reflects when the database was published by IP2Location, typically the 1st of the month.

      Returns:
      The database date, or null if no IPv6 database is loaded, IPv6 is not enabled, or no service is configured
    • getNextScheduledUpdate

      public Instant getNextScheduledUpdate()
      Returns the instant when the next scheduled update check will occur.

      This reflects the scheduler's next planned execution, not necessarily when a download will occur (which depends on the current month and whether an update is needed).

      Returns:
      The instant of the next scheduled check, or null if no service is configured or the scheduler is not running
    • getStatusDescription

      public String getStatusDescription()
      Returns the current status as a human-readable string for UI display.

      The status components are concatenated in order of importance, separated by ", " (comma and space). This includes both manager-level and service-level status components.

      Manager-level components (in order of importance):

      • Shutdown - Manager has been shut down
      • Not configured - No service has been configured yet
      • Reconfiguring - Hot-swap reconfiguration in progress
      • Draining - Old instance completing in-flight requests

      Service-level components (delegated from active service):

      • Disposed, Disabled, Initializing
      • IPv4 only / IPv4+IPv6
      • Downloading IPv4 / Downloading IPv6
      • IPv4 ready / IPv4 not loaded
      • IPv6 ready / IPv6 not loaded

      Example outputs:

      • "Shutdown"
      • "Not configured"
      • "Reconfiguring, Draining, IPv4+IPv6, IPv4 ready, IPv6 ready"
      • "IPv4 only, Downloading IPv4"
      • "IPv4+IPv6, IPv4 ready, IPv6 ready"
      Returns:
      A comma-separated status string, never null
    • isRecoveryRecommended

      public boolean isRecoveryRecommended()
      Returns whether recovery mode is recommended.

      Recovery is recommended when the service encounters persistent errors that suggest corrupted database files or state. Conditions that trigger this recommendation:

      • Two or more consecutive initialization failures
      • Ten or more consecutive lookup errors (DB_ERROR status)
      • Database file validation failures

      When recovery is recommended, the UI should offer the user an option to reconfigure with attemptRecovery() enabled.

      Example

      if (manager.isRecoveryRecommended()) {
          String reason = manager.getRecoveryReason();
          boolean userConfirmed = showConfirmation(
              "Recovery Recommended",
              reason + "\n\nAttempt recovery?");
          
          if (userConfirmed) {
              manager.reconfigure(new IP2LocationService.Builder()
                  .downloadToken(token)
                  .attemptRecovery()  // Clears corrupted files
                  .build());
          }
      }
      
      Returns:
      true if recovery is recommended
      See Also:
    • getRecoveryReason

      public String getRecoveryReason()
      Returns the reason why recovery is recommended.

      This provides a human-readable explanation suitable for display in a UI dialog or log message.

      Returns:
      The reason string, or null if recovery is not recommended
      See Also:
    • getConsecutiveInitFailures

      public int getConsecutiveInitFailures()
      Returns the count of consecutive initialization failures.

      This counter is incremented each time configuration or reconfiguration fails, and reset to 0 on successful initialization.

      Returns:
      Number of consecutive init failures
    • getConsecutiveLookupErrors

      public int getConsecutiveLookupErrors()
      Returns the count of consecutive lookup errors.

      This counter is incremented when lookups return DB_ERROR status, and reset to 0 on any successful lookup.

      Returns:
      Number of consecutive lookup errors
    • clearRecoveryRecommendation

      public void clearRecoveryRecommendation()
      Clears the recovery recommendation.

      Call this after the user dismisses the recovery recommendation, or after a successful recovery. The recommendation will be re-raised if errors continue.

    • addStatusListener

      public void addStatusListener(IServiceStatusListener listener)
      Adds a status listener to receive service events.

      The listener will be notified of events such as initialization, downloads, database updates, and reconfiguration.

      This method is thread-safe and idempotent - adding the same listener multiple times has no additional effect.

      Example

      manager.addStatusListener(new IServiceStatusListener() {
          @Override
          public void onDatabaseUpdated(boolean ipv4, boolean ipv6) {
              System.out.println("Databases updated!");
          }
      });
      
      Parameters:
      listener - The listener to add (must not be null)
      Throws:
      NullPointerException - if listener is null
      See Also:
    • removeStatusListener

      public void removeStatusListener(IServiceStatusListener listener)
      Removes a status listener.

      The listener will no longer receive events after this call returns.

      This method is thread-safe and idempotent - removing a listener that is not registered has no effect.

      Parameters:
      listener - The listener to remove (must not be null)
      Throws:
      NullPointerException - if listener is null
      See Also:
    • setRecoveryConfig

      public void setRecoveryConfig(RecoveryConfig config)
      Sets the recovery configuration.

      This configuration controls how the manager responds to errors and when/whether automatic recovery is triggered.

      Example

      manager.setRecoveryConfig(RecoveryConfig.builder()
          .recoveryAction(RecoveryConfig.RecoveryAction.AUTOMATIC)
          .initFailureThreshold(3)
          .lookupErrorThreshold(20)
          .recoverOnDownloadFailure(true)
          .recoverOnCorruption(true)
          .maxAutoRecoveryAttempts(3)
          .autoRecoveryCooldownMinutes(60)
          .build());
      
      Parameters:
      config - The recovery configuration (must not be null)
      Throws:
      NullPointerException - if config is null
      See Also:
    • getRecoveryConfig

      public RecoveryConfig getRecoveryConfig()
      Returns the current recovery configuration.
      Returns:
      The current RecoveryConfig (never null)
      See Also:
    • canTriggerRecovery

      public boolean canTriggerRecovery()
      Checks whether manual recovery can be triggered now.

      Returns true if all of the following conditions are met:

      • Manager is not shut down
      • A configuration builder is available (service was configured)
      • Recovery is not already in progress
      • Cooldown period has elapsed since last attempt (if any)
      Returns:
      true if triggerRecovery() can be called
      See Also:
    • isRecoveryInProgress

      public boolean isRecoveryInProgress()
      Returns whether recovery is currently in progress.
      Returns:
      true if recovery is in progress
    • getAutoRecoveryAttempts

      public int getAutoRecoveryAttempts()
      Returns the number of automatic recovery attempts made.

      This counter is incremented each time automatic recovery is triggered and reset when recovery succeeds or when manually cleared.

      Returns:
      Number of auto-recovery attempts
    • triggerRecovery

      public void triggerRecovery()
      Triggers a manual recovery attempt.

      This clears the database directory and re-downloads fresh databases. The operation runs asynchronously. Use canTriggerRecovery() to check if this method can be called.

      Recovery involves:

      1. Disposing the current service instance
      2. Clearing the database directory (BIN files, state file, temp files)
      3. Creating a new service with attemptRecovery() enabled
      4. Waiting for the new service to download and initialize

      Example

      if (manager.canTriggerRecovery()) {
          manager.triggerRecovery();
      }
      
      Throws:
      IllegalStateException - if recovery cannot be triggered
      See Also:
    • canUpdateDatabasesNow

      public boolean canUpdateDatabasesNow()
      Checks whether databases can be updated now.

      Returns true if all of the following conditions are met:

      • Manager is not shut down
      • A service is configured
      • Service is enabled
      • Service is running

      Note: This does not check if a download is already in progress. Calling updateDatabasesNow() while a download is in progress is safe but will queue another download.

      Returns:
      true if updateDatabasesNow() can be called
      See Also:
    • updateDatabasesNow

      public void updateDatabasesNow()
      Triggers an immediate database update.

      This bypasses the normal scheduling logic and attempts to download the latest databases immediately. The operation runs asynchronously.

      Use canUpdateDatabasesNow() to check if this method can be called successfully.

      Example

      if (manager.canUpdateDatabasesNow()) {
          manager.updateDatabasesNow();
          // Status will change to "Downloading IPv4" etc.
      }
      
      Throws:
      IllegalStateException - if no service is configured or service is not in a state that allows updates
      See Also:
    • setLogger

      public void setLogger(IEventLogger logger)
      Sets the event logger for operational messages.

      This logger is used for manager-level events. Each service instance has its own logger configured via its builder.

      Parameters:
      logger - The logger implementation, or null for console logging
    • shutdown

      public void shutdown()
      Shuts down the manager and all active services.

      This method should be called during application shutdown. It:

      1. Disposes any draining instance immediately
      2. Disposes the active instance
      3. Marks the manager as shut down

      After calling this method, the manager cannot be reconfigured. Note: As this is a singleton, shutting down affects the entire application.