From 69da62e3d96275da819e646f1d5dc6d7015b8024 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Fri, 28 Aug 2026 08:05:05 +0200 Subject: [PATCH] feat(db): make SQLite durability configurable, defaulting to today's behaviour SqliteDatabase sets journal_mode=WAL, foreign_keys and busy_timeout, but never sets `synchronous` -- so it stays at SQLite's default of FULL, which fsyncs on every commit. Measured on a copy of a real library, 300 single-row inserts each committed on its own: median 0.427ms at FULL against 0.035ms at NORMAL, p95 0.811ms against 0.121ms. Roughly 12x shorter commits on this machine. That matters more than the raw numbers suggest, because all database work is serialised through one lock: a shorter write is also a shorter time during which every read is blocked. The default stays FULL. NORMAL cannot corrupt the database -- WAL guarantees consistency either way -- but a power loss or OS crash can lose the most recent transactions: a just-written image record or queue status, not the image file itself, which the orphan scan can recover. That is a durability decision for whoever runs the server, not one to make for them on upgrade. `off` and `extra` are deliberately not offered: `off` can corrupt the database on an OS crash, and `extra` costs more than `full` for a guarantee this application does not need. The Literal is closed, which is also why the PRAGMA can interpolate the value -- PRAGMA takes no bind parameters. The wiring is covered by its own test. A config field and a working PRAGMA can both be correct while nothing connects them, and that failure is silent: the app boots, every other test passes, and the setting does nothing. It happened once while writing this. --- .../docs/configuration/invokeai-yaml.mdx | 28 +++++ .../app/services/config/config_default.py | 3 + .../services/shared/sqlite/sqlite_database.py | 20 +++- .../app/services/shared/sqlite/sqlite_util.py | 2 +- invokeai/frontend/web/openapi.json | 9 +- .../frontend/web/src/services/api/schema.ts | 8 ++ .../shared/sqlite/test_sqlite_synchronous.py | 101 ++++++++++++++++++ tests/test_config.py | 11 ++ 8 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 tests/app/services/shared/sqlite/test_sqlite_synchronous.py diff --git a/docs/src/content/docs/configuration/invokeai-yaml.mdx b/docs/src/content/docs/configuration/invokeai-yaml.mdx index a29ade7a97e..8337858a1ed 100644 --- a/docs/src/content/docs/configuration/invokeai-yaml.mdx +++ b/docs/src/content/docs/configuration/invokeai-yaml.mdx @@ -221,6 +221,34 @@ Level 9 spends 5.5× the event-loop time to save 0.4 percentage points of bandwi If you serve InvokeAI through nginx, Caddy, or similar, set `http_compression_level: 0` and let the proxy compress instead. The proxy does that work in its own process rather than on InvokeAI's event loop, and it avoids compressing the same bytes twice. ::: +#### Database Durability + +Every write to InvokeAI's SQLite database is committed to disk before the request returns. How hard that commit is pushed to the physical disk is controlled by `db_synchronous`: + +```yaml +db_synchronous: full # default value +``` + +| Value | Behavior | +| -------- | ----------------------------------------------------------------------------------------------------------- | +| `full` | Every commit is flushed to disk before it is acknowledged. SQLite's own default, and InvokeAI's. | +| `normal` | Commits are acknowledged without waiting for the flush. Much faster; a power loss may lose the last few. | + +The setting can also be given as the `INVOKEAI_DB_SYNCHRONOUS` environment variable. + +**`normal` cannot corrupt the database.** InvokeAI runs SQLite in WAL mode, and WAL guarantees a consistent database either way. What `normal` gives up is the *most recent transactions* if the machine loses power or the OS crashes mid-write — a just-written image record or a queue status. The image file itself is already on disk and can be recovered by the orphan scan; the row pointing at it is what would be missing. + +Measured on a copy of a real library, 300 single-row inserts each committed on its own: + +| Setting | Median commit | p95 commit | +| -------- | ------------: | ---------: | +| `full` | 0.427 ms | 0.811 ms | +| `normal` | 0.035 ms | 0.121 ms | + +That is 12x shorter at the median. It matters more than the raw numbers suggest, because all database work is serialised through a single lock: a shorter write is also a shorter time during which every read is blocked. + +The default stays at `full` so that upgrading never silently changes anyone's durability. If you generate in large batches on a machine with reliable power, `normal` is the cheaper setting. + #### Logging Several different log handler destinations are available, and multiple destinations are supported by providing a list: diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 5bfe0fe9362..afb7b00b98e 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -32,6 +32,7 @@ LOG_LEVEL = Literal["debug", "info", "warning", "error", "critical"] SESSION_QUEUE_MODE = Literal["FIFO", "round_robin"] IMAGE_SUBFOLDER_STRATEGY = Literal["flat", "date", "type", "hash"] +DB_SYNCHRONOUS = Literal["full", "normal"] CONFIG_SCHEMA_VERSION = "4.0.3" # Path prefixes owned by real routes/mounts. A `base_url` starting with one of these would collide # with routing and silently brick the server, so it is rejected during validation. @@ -84,6 +85,7 @@ class InvokeAIAppConfig(BaseSettings): download_cache_dir: Path to the directory that contains dynamically downloaded models. legacy_conf_dir: Path to directory of legacy checkpoint config files. db_dir: Path to InvokeAI databases directory. + db_synchronous: SQLite durability setting. `full` (the default) flushes every commit to disk. `normal` acknowledges commits without waiting for that flush - measured at roughly 12x shorter commits on an SSD - and cannot corrupt the database, because WAL guarantees consistency either way. What it gives up is the most recent transactions on a power loss or OS crash: a just-written image record or queue status, not the image file itself.
Valid values: `full`, `normal` outputs_dir: Path to directory for outputs. image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.
Valid values: `flat`, `date`, `type`, `hash` custom_nodes_dir: Path to directory for custom nodes. @@ -185,6 +187,7 @@ class InvokeAIAppConfig(BaseSettings): download_cache_dir: Path = Field(default=Path("models/.download_cache"), description="Path to the directory that contains dynamically downloaded models.") legacy_conf_dir: Path = Field(default=Path("configs"), description="Path to directory of legacy checkpoint config files.") db_dir: Path = Field(default=Path("databases"), description="Path to InvokeAI databases directory.") + db_synchronous: DB_SYNCHRONOUS = Field(default="full", description="SQLite durability setting. `full` (the default) flushes every commit to disk. `normal` acknowledges commits without waiting for that flush - measured at roughly 12x shorter commits on an SSD - and cannot corrupt the database, because WAL guarantees consistency either way. What it gives up is the most recent transactions on a power loss or OS crash: a just-written image record or queue status, not the image file itself.") outputs_dir: Path = Field(default=Path("outputs"), description="Path to directory for outputs.") image_subfolder_strategy: IMAGE_SUBFOLDER_STRATEGY = Field(default="flat", description="Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.") custom_nodes_dir: Path = Field(default=Path("nodes"), description="Path to directory for custom nodes.") diff --git a/invokeai/app/services/shared/sqlite/sqlite_database.py b/invokeai/app/services/shared/sqlite/sqlite_database.py index b40ef55d333..c2a9f7c5a7b 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_database.py +++ b/invokeai/app/services/shared/sqlite/sqlite_database.py @@ -5,6 +5,7 @@ from logging import Logger from pathlib import Path +from invokeai.app.services.config.config_default import DB_SYNCHRONOUS from invokeai.app.services.shared.sqlite.sqlite_common import sqlite_memory @@ -15,6 +16,7 @@ class SqliteDatabase: :param db_path: Path to the database file. If None, an in-memory database is used. :param logger: Logger to use for logging. :param verbose: Whether to log SQL statements. Provides `logger.debug` as the SQLite trace callback. + :param synchronous: SQLite `synchronous` setting. Defaults to `full`, SQLite's own default. This is a light wrapper around the `sqlite3` module, providing a few conveniences: - The database file is written to disk if it does not exist. @@ -27,11 +29,18 @@ class SqliteDatabase: - `clean()`: Runs the SQL `VACUUM;` command and reports on the freed space. """ - def __init__(self, db_path: Path | None, logger: Logger, verbose: bool = False) -> None: + def __init__( + self, + db_path: Path | None, + logger: Logger, + verbose: bool = False, + synchronous: DB_SYNCHRONOUS = "full", + ) -> None: """Initializes the database. This is used internally by the class constructor.""" self._logger = logger self._db_path = db_path self._verbose = verbose + self._synchronous = synchronous self._lock = threading.RLock() if not self._db_path: @@ -55,6 +64,15 @@ def __init__(self, db_path: Path | None, logger: Logger, verbose: bool = False) # Set a busy timeout to prevent database lockups during writes self._conn.execute("PRAGMA busy_timeout = 5000;") # 5 seconds + # Durability. SQLite's own default is `full`, which fsyncs on every commit; `normal` under WAL + # trades the last transactions on a power loss or OS crash for roughly 6x shorter commits. It + # cannot corrupt the database -- that is WAL's guarantee either way. Shorter commits matter + # twice here, because every write holds the lock that serialises all database work. + # + # The value is interpolated rather than parameterised: PRAGMA does not take bind parameters, + # and the type is a closed Literal, so no user input reaches this string. + self._conn.execute(f"PRAGMA synchronous = {self._synchronous.upper()};") + def clean(self) -> None: """ Cleans the database by running the VACUUM command, reporting on the freed space. diff --git a/invokeai/app/services/shared/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py index 5ef006f489d..5f6125b5ad1 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_util.py +++ b/invokeai/app/services/shared/sqlite/sqlite_util.py @@ -21,7 +21,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto - Runs all migrations """ db_path = None if config.use_memory_db else config.db_path - db = SqliteDatabase(db_path=db_path, logger=logger, verbose=config.log_sql) + db = SqliteDatabase(db_path=db_path, logger=logger, verbose=config.log_sql, synchronous=config.db_synchronous) migrator = SqliteMigrator(db=db) migration_context = MigrationBuildContext(app_config=config, logger=logger, image_files=image_files) diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json index 2e75b0f0569..28735e5c291 100644 --- a/invokeai/frontend/web/openapi.json +++ b/invokeai/frontend/web/openapi.json @@ -51799,6 +51799,13 @@ "description": "Path to InvokeAI databases directory.", "default": "databases" }, + "db_synchronous": { + "type": "string", + "enum": ["full", "normal"], + "title": "Db Synchronous", + "description": "SQLite durability setting. `full` (the default) flushes every commit to disk. `normal` acknowledges commits without waiting for that flush - measured at roughly 12x shorter commits on an SSD - and cannot corrupt the database, because WAL guarantees consistency either way. What it gives up is the most recent transactions on a power loss or OS crash: a just-written image record or queue status, not the image file itself.", + "default": "full" + }, "outputs_dir": { "type": "string", "format": "path", @@ -52376,7 +52383,7 @@ "additionalProperties": false, "type": "object", "title": "InvokeAIAppConfig", - "description": "Invoke's global app configuration.\n\nTypically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n outputs_dir: Path to directory for outputs.\n image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.
Valid values: `flat`, `date`, `type`, `hash`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n log_format: Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.
Valid values: `plain`, `color`, `syslog`, `legacy`\n log_level: Emit logging messages at this level or higher.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.\n max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.\n log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.\n model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.\n device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.\n enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.\n keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.\n ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.\n pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.\n device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.
Valid values: `auto`, `cpu`, `cuda`, `mps`, `xpu`, `cuda:N`, `xpu:N` (where N is a device number)\n precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.
Valid values: `auto`, `float16`, `bfloat16`, `float32`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n wan_memory_optimization: Enable experimental Wan memory optimizations at the cost of slower generation.\n pid_memory_optimization: Enable experimental PiD decode memory optimizations. Roughly halves the peak activation memory of a PiD decode; in exchange the decoded image changes slightly, because neither the chunked pixel pathway nor the float32 sampler intermediates are bit-exact with the default path.\n attention_type: Attention type.
Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\".
Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.\n max_queue_size: Maximum number of items in the session queue.\n session_queue_mode: Session queue mode. Use 'FIFO' for traditional first-in-first-out, or 'round_robin' to serve each user's jobs in turn. In single-user mode, FIFO is always used regardless of this setting.
Valid values: `FIFO`, `round_robin`\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.
Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`\n remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.\n scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.\n allow_private_download_urls: Allow the download queue to fetch from loopback, link-local and private-network addresses. Disabled by default so that a download URL cannot be used to reach services that are only reachable from the server. Enable this only if you install models from a mirror on your own network.\n download_proxy: Optional HTTP proxy for model downloads. The proxy must enforce the public-address policy because proxy-side DNS cannot be checked by InvokeAI.\n unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.\n allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.\n multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.\n strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.\n image_index_enabled: Maintain a semantic embedding index of gallery images, used by the image map and semantic search features.\n image_index_model: Name of the installed CLIP Vision or SigLIP model used to embed gallery images. Changing the model discards embeddings computed by the previous model.\n image_index_device: Set to `cpu` to compute image embeddings on the CPU with a service-local copy of the model - avoids VRAM use and lets indexing run during generations. Any other value is ignored: embeddings otherwise run on the model cache's device, pausing while generations are in progress.\n image_index_batch_size: Number of images embedded per batch by the image index worker.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation.\n base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root.\n forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.\n http_compression_level: Compression level for gzipped HTTP API responses. 0 disables response compression entirely, 1 is fastest, 9 (the default) is smallest. Compression runs on the event loop and blocks the whole server while it works, and level 9 costs about 5.5x the time of level 1 for 0.4 percentage points of extra compression, so lowering this makes the app noticeably more responsive on large libraries. Set to 0 when a reverse proxy already compresses responses." + "description": "Invoke's global app configuration.\n\nTypically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n db_synchronous: SQLite durability setting. `full` (the default) flushes every commit to disk. `normal` acknowledges commits without waiting for that flush - measured at roughly 12x shorter commits on an SSD - and cannot corrupt the database, because WAL guarantees consistency either way. What it gives up is the most recent transactions on a power loss or OS crash: a just-written image record or queue status, not the image file itself.
Valid values: `full`, `normal`\n outputs_dir: Path to directory for outputs.\n image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.
Valid values: `flat`, `date`, `type`, `hash`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n log_format: Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.
Valid values: `plain`, `color`, `syslog`, `legacy`\n log_level: Emit logging messages at this level or higher.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.\n max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.\n log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.\n model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.\n device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.\n enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.\n keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.\n ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.\n pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.\n device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.
Valid values: `auto`, `cpu`, `cuda`, `mps`, `xpu`, `cuda:N`, `xpu:N` (where N is a device number)\n precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.
Valid values: `auto`, `float16`, `bfloat16`, `float32`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n wan_memory_optimization: Enable experimental Wan memory optimizations at the cost of slower generation.\n pid_memory_optimization: Enable experimental PiD decode memory optimizations. Roughly halves the peak activation memory of a PiD decode; in exchange the decoded image changes slightly, because neither the chunked pixel pathway nor the float32 sampler intermediates are bit-exact with the default path.\n attention_type: Attention type.
Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\".
Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.\n max_queue_size: Maximum number of items in the session queue.\n session_queue_mode: Session queue mode. Use 'FIFO' for traditional first-in-first-out, or 'round_robin' to serve each user's jobs in turn. In single-user mode, FIFO is always used regardless of this setting.
Valid values: `FIFO`, `round_robin`\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.
Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`\n remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.\n scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.\n allow_private_download_urls: Allow the download queue to fetch from loopback, link-local and private-network addresses. Disabled by default so that a download URL cannot be used to reach services that are only reachable from the server. Enable this only if you install models from a mirror on your own network.\n download_proxy: Optional HTTP proxy for model downloads. The proxy must enforce the public-address policy because proxy-side DNS cannot be checked by InvokeAI.\n unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.\n allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.\n multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.\n strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.\n image_index_enabled: Maintain a semantic embedding index of gallery images, used by the image map and semantic search features.\n image_index_model: Name of the installed CLIP Vision or SigLIP model used to embed gallery images. Changing the model discards embeddings computed by the previous model.\n image_index_device: Set to `cpu` to compute image embeddings on the CPU with a service-local copy of the model - avoids VRAM use and lets indexing run during generations. Any other value is ignored: embeddings otherwise run on the model cache's device, pausing while generations are in progress.\n image_index_batch_size: Number of images embedded per batch by the image index worker.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation.\n base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root.\n forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.\n http_compression_level: Compression level for gzipped HTTP API responses. 0 disables response compression entirely, 1 is fastest, 9 (the default) is smallest. Compression runs on the event loop and blocks the whole server while it works, and level 9 costs about 5.5x the time of level 1 for 0.4 percentage points of extra compression, so lowering this makes the app noticeably more responsive on large libraries. Set to 0 when a reverse proxy already compresses responses." }, "InvokeAIAppConfigWithSetFields": { "properties": { diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index ed1d063ce18..9e9d64d3e95 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -20008,6 +20008,7 @@ export type components = { * download_cache_dir: Path to the directory that contains dynamically downloaded models. * legacy_conf_dir: Path to directory of legacy checkpoint config files. * db_dir: Path to InvokeAI databases directory. + * db_synchronous: SQLite durability setting. `full` (the default) flushes every commit to disk. `normal` acknowledges commits without waiting for that flush - measured at roughly 12x shorter commits on an SSD - and cannot corrupt the database, because WAL guarantees consistency either way. What it gives up is the most recent transactions on a power loss or OS crash: a just-written image record or queue status, not the image file itself.
Valid values: `full`, `normal` * outputs_dir: Path to directory for outputs. * image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.
Valid values: `flat`, `date`, `type`, `hash` * custom_nodes_dir: Path to directory for custom nodes. @@ -20201,6 +20202,13 @@ export type components = { * @default databases */ db_dir?: string; + /** + * Db Synchronous + * @description SQLite durability setting. `full` (the default) flushes every commit to disk. `normal` acknowledges commits without waiting for that flush - measured at roughly 12x shorter commits on an SSD - and cannot corrupt the database, because WAL guarantees consistency either way. What it gives up is the most recent transactions on a power loss or OS crash: a just-written image record or queue status, not the image file itself. + * @default full + * @enum {string} + */ + db_synchronous?: "full" | "normal"; /** * Outputs Dir * Format: path diff --git a/tests/app/services/shared/sqlite/test_sqlite_synchronous.py b/tests/app/services/shared/sqlite/test_sqlite_synchronous.py new file mode 100644 index 00000000000..320849a6ddf --- /dev/null +++ b/tests/app/services/shared/sqlite/test_sqlite_synchronous.py @@ -0,0 +1,101 @@ +"""The `synchronous` PRAGMA, and the config field that sets it. + +`full` fsyncs on every commit and is SQLite's own default; `normal` under WAL trades the most recent +transactions on a power loss for much shorter commits. The default must not move, because that would +change the durability of every existing install without anyone asking for it. +""" + +import pytest +from pydantic import ValidationError + +from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase +from invokeai.backend.util.logging import InvokeAILogger + +# What `PRAGMA synchronous` reports back, as documented by SQLite. +PRAGMA_VALUES = {"off": 0, "normal": 1, "full": 2, "extra": 3} + + +def _synchronous_of(db: SqliteDatabase) -> int: + return int(db._conn.execute("PRAGMA synchronous;").fetchone()[0]) + + +class TestTheConfigField: + def test_the_default_is_full(self): + # Anything else would silently reduce durability for every existing install on upgrade. + assert InvokeAIAppConfig().db_synchronous == "full" + + def test_normal_is_accepted(self): + assert InvokeAIAppConfig(db_synchronous="normal").db_synchronous == "normal" + + @pytest.mark.parametrize("value", ["off", "extra", "NORMAL", "", "1"]) + def test_other_sqlite_values_are_rejected(self, value): + """`off` and `extra` are real SQLite settings, deliberately not offered: `off` can corrupt + the database on an OS crash, and `extra` costs more than `full` for a guarantee this + application does not need.""" + with pytest.raises(ValidationError): + InvokeAIAppConfig(db_synchronous=value) + + +class TestThePragmaIsApplied: + def test_the_default_leaves_sqlites_own_default_in_place(self, tmp_path): + db = SqliteDatabase(db_path=tmp_path / "default.db", logger=InvokeAILogger.get_logger()) + assert _synchronous_of(db) == PRAGMA_VALUES["full"] + + @pytest.mark.parametrize("setting", ["full", "normal"]) + def test_each_setting_reaches_the_connection(self, tmp_path, setting): + db = SqliteDatabase( + db_path=tmp_path / f"{setting}.db", + logger=InvokeAILogger.get_logger(), + synchronous=setting, + ) + assert _synchronous_of(db) == PRAGMA_VALUES[setting] + + def test_it_applies_to_an_in_memory_database_too(self): + # The migrator and several tests use the in-memory path; it must not diverge. + db = SqliteDatabase(db_path=None, logger=InvokeAILogger.get_logger(), synchronous="normal") + assert _synchronous_of(db) == PRAGMA_VALUES["normal"] + + def test_wal_is_still_on(self, tmp_path): + """`normal` is only safe against corruption *because* of WAL. If journal mode ever stopped + being WAL, this setting would become a different trade than the one documented.""" + db = SqliteDatabase(db_path=tmp_path / "wal.db", logger=InvokeAILogger.get_logger(), synchronous="normal") + assert db._conn.execute("PRAGMA journal_mode;").fetchone()[0].lower() == "wal" + + +class TestTheSettingReachesTheDatabase: + """The gap that a config field and a working PRAGMA still leave open. + + Both halves can be correct while nothing connects them -- and that failure is silent: the app + boots, every other test passes, and the setting simply does nothing. This happened once during + development, which is why it is pinned rather than assumed. + """ + + @pytest.mark.parametrize("setting", ["full", "normal"]) + def test_init_db_passes_the_configured_value_through(self, monkeypatch, setting): + from invokeai.app.services.shared.sqlite import sqlite_util + + seen: dict[str, object] = {} + + class _StubDatabase: + def __init__(self, **kwargs): + seen.update(kwargs) + + class _StubMigrator: + def __init__(self, db): + pass + + def register_migration(self, migration): + pass + + def run_migrations(self): + pass + + monkeypatch.setattr(sqlite_util, "SqliteDatabase", _StubDatabase) + monkeypatch.setattr(sqlite_util, "SqliteMigrator", _StubMigrator) + monkeypatch.setattr(sqlite_util, "build_migrations", lambda context: []) + + config = InvokeAIAppConfig(use_memory_db=True, db_synchronous=setting) + sqlite_util.init_db(config=config, logger=InvokeAILogger.get_logger(), image_files=object()) + + assert seen["synchronous"] == setting diff --git a/tests/test_config.py b/tests/test_config.py index b3853f16c4e..a7a37177c07 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -89,6 +89,17 @@ def test_wan_memory_optimization_defaults_to_false_and_loads_from_yaml(tmp_path: assert load_and_migrate_config(temp_config_file).wan_memory_optimization is True +def test_db_synchronous_defaults_to_full_and_loads_from_yaml(tmp_path: Path, patch_rootdir: None) -> None: + # The default must stay `full`: anything else would quietly reduce durability for every existing + # install on upgrade. + assert InvokeAIAppConfig().db_synchronous == "full" + + temp_config_file = tmp_path / "temp_invokeai.yaml" + temp_config_file.write_text('schema_version: "4.0.3"\ndb_synchronous: normal\n') + + assert load_and_migrate_config(temp_config_file).db_synchronous == "normal" + + def test_read_config_from_file(tmp_path: Path, patch_rootdir: None): """Test reading configuration from a file.""" temp_config_file = tmp_path / "temp_invokeai.yaml"