Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/src/content/docs/configuration/invokeai-yaml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions invokeai/app/services/config/config_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.<br>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.<br>Valid values: `flat`, `date`, `type`, `hash`
custom_nodes_dir: Path to directory for custom nodes.
Expand Down Expand Up @@ -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.")
Expand Down
20 changes: 19 additions & 1 deletion invokeai/app/services/shared/sqlite/sqlite_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion invokeai/app/services/shared/sqlite/sqlite_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading