Przejdź do treści

♻️ Idempotent Operations Patterns

Idempotent operations produce the same result regardless of how many times they're executed. This pattern is essential for reliable automation, infrastructure as code, and distributed systems where operations may need to be retried.


🎯 Core Principles

Deterministic Outcomes

Operations should have predictable results regardless of execution frequency.

1
2
3
4
5
6
7
8
9
# ❌ Non-idempotent
create_user() {
    useradd alice  # Fails on second run if user exists
}

# ✅ Idempotent
create_user() {
    id alice >/dev/null 2>&1 || useradd alice
}

State Inspection Before Action

Always check current state before making changes.

1
2
3
4
5
6
7
8
9
# Generic idempotent pattern
ensure_state() {
    local current_state=$(get_current_state)
    local desired_state="$1"

    if [ "$current_state" != "$desired_state" ]; then
        apply_change "$desired_state"
    fi
}

🔧 File System Operations

Directory Creation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ❌ Non-idempotent
mkdir /app/data

# ✅ Idempotent
mkdir -p /app/data

# More explicit version
ensure_directory() {
    local dir="$1"
    if [ ! -d "$dir" ]; then
        mkdir -p "$dir"
        echo "Created directory: $dir"
    else
        echo "Directory already exists: $dir"
    fi
}

File Content Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ❌ Non-idempotent - always writes
echo "config=value" > /etc/myapp.conf

# ✅ Idempotent - only writes if different
write_config_idempotent() {
    local file="/etc/myapp.conf"
    local content="config=value"

    if [ ! -f "$file" ] || [ "$content" != "$(cat "$file" 2>/dev/null)" ]; then
        echo "$content" > "$file"
        echo "Updated configuration: $file"
    else
        echo "Configuration unchanged: $file"
    fi
}

File Permissions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ❌ Non-idempotent - always changes
chmod 644 /etc/myapp.conf

# ✅ Idempotent - only changes if needed
set_permissions_idempotent() {
    local file="$1"
    local desired_perms="$2"

    local current_perms=$(stat -c "%a" "$file" 2>/dev/null)

    if [ "$current_perms" != "$desired_perms" ]; then
        chmod "$desired_perms" "$file"
        echo "Set permissions $desired_perms on $file"
    else
        echo "Permissions already correct: $file"
    fi
}

🛠️ Package Management

Package Installation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ❌ Non-idempotent - may fail or reinstall
apt-get install nginx

# ✅ Idempotent - checks before installing
install_package_idempotent() {
    local package="$1"

    if ! dpkg -l "$package" >/dev/null 2>&1; then
        apt-get update
        apt-get install -y "$package"
        echo "Installed package: $package"
    else
        echo "Package already installed: $package"
    fi
}

# Cross-platform version
ensure_package() {
    local package="$1"

    case "$(uname -s)" in
        Linux*)
            if command -v apt-get >/dev/null 2>&1; then
                dpkg -l "$package" >/dev/null 2>&1 || {
                    apt-get update && apt-get install -y "$package"
                }
            elif command -v yum >/dev/null 2>&1; then
                rpm -q "$package" >/dev/null 2>&1 || {
                    yum install -y "$package"
                }
            fi
            ;;
    esac
}

<|fim_pad|>

Service Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ❌ Non-idempotent
systemctl start nginx

# ✅ Idempotent
ensure_service_running() {
    local service="$1"

    if ! systemctl is-active --quiet "$service"; then
        systemctl start "$service"
        echo "Started service: $service"
    else
        echo "Service already running: $service"
    fi
}

# Ensure service is enabled and running
ensure_service() {
    local service="$1"

    # Enable service
    if ! systemctl is-enabled --quiet "$service"; then
        systemctl enable "$service"
        echo "Enabled service: $service"
    fi

    # Start service
    if ! systemctl is-active --quiet "$service"; then
        systemctl start "$service"
        echo "Started service: $service"
    else
        echo "Service already running: $service"
    fi
}

🌐 Network Configuration

Firewall Rules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ❌ Non-idempotent - adds duplicate rules
iptables -A INPUT -p tcp --dport 80 -j ACCEPT

# ✅ Idempotent - checks before adding
ensure_firewall_rule() {
    local rule="$1"

    if ! iptables -C $rule 2>/dev/null; then
        iptables -A $rule
        echo "Added firewall rule: $rule"
    else
        echo "Firewall rule already exists: $rule"
    fi
}

# Usage
ensure_firewall_rule "INPUT -p tcp --dport 80 -j ACCEPT"

Hosts File Entries

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ✅ Idempotent hosts file management
ensure_hosts_entry() {
    local ip="$1"
    local hostname="$2"
    local hosts_file="${3:-"/etc/hosts"}"

    local entry="$ip $hostname"

    if ! grep -q "^$ip\s\+$hostname" "$hosts_file" 2>/dev/null; then
        echo "$entry" >> "$hosts_file"
        echo "Added hosts entry: $entry"
    else
        echo "Hosts entry already exists: $entry"
    fi
}

🗃️ Data Operations

Database Records

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# ✅ Idempotent database operations
ensure_database_record() {
    local table="$1"
    local condition="$2"
    local insert_sql="$3"

    # Check if record exists
    if ! mysql -e "SELECT 1 FROM $table WHERE $condition" | grep -q "1"; then
        mysql -e "$insert_sql"
        echo "Inserted record into $table"
    else
        echo "Record already exists in $table"
    fi
}

# Usage
ensure_database_record "users" "username='admin'" \
    "INSERT INTO users (username, role) VALUES ('admin', 'administrator')"

Configuration Values

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# ✅ Idempotent configuration management
ensure_config_value() {
    local config_file="$1"
    local key="$2"
    local value="$3"

    local pattern="^${key}\s*="

    if grep -q "$pattern" "$config_file" 2>/dev/null; then
        # Update existing value
        sed -i "s/$pattern.*/$key=$value/" "$config_file"
        echo "Updated $key in $config_file"
    else
        # Add new value
        echo "$key=$value" >> "$config_file"
        echo "Added $key to $config_file"
    fi
}

🎨 Advanced Patterns

Transactional Operations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Idempotent operations with rollback capability
idempotent_transaction() {
    local operation_name="$1"
    local check_fn="$2"
    local apply_fn="$3"
    local rollback_fn="$4"

    if ! $check_fn; then
        echo "Executing $operation_name..."

        if $apply_fn; then
            echo "Completed $operation_name"
        else
            echo "Failed $operation_name, attempting rollback..."
            $rollback_fn
            return 1
        fi
    else
        echo "$operation_name already completed"
    fi
}

# Example usage
check_user_exists() {
    id alice >/dev/null 2>&1
}

create_user_safe() {
    useradd alice
}

rollback_user_creation() {
    userdel alice 2>/dev/null || true
}

idempotent_transaction "Create user alice" \
    check_user_exists \
    create_user_safe \
    rollback_user_creation

Batch Idempotent Operations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Process multiple items idempotently
ensure_multiple_items() {
    local items=("$@")
    local ensure_fn="$1"
    shift

    for item in "${items[@]}"; do
        $ensure_fn "$item"
    done
}

# Usage
packages=("nginx" "postgresql" "redis")
ensure_multiple_items ensure_package "${packages[@]}"

services=("nginx" "postgresql" "redis")
ensure_multiple_items ensure_service "${services[@]}"

🧾 Summary Best Practices

Implementation Guidelines

✅ Always check current state before making changes ✅ Use descriptive feedback messages ✅ Handle edge cases gracefully ✅ Provide dry-run capabilities ✅ Log operations for audit trails

Common Patterns

  1. Check-Then-Act: Verify state before modification
  2. Compare-Then-Update: Only modify if different
  3. Existence-Then-Create: Only create if absent
  4. Status-Then-Change: Only change if needed

Anti-Patterns to Avoid

❌ Assuming operations are safe to repeat ❌ Ignoring existing state ❌ Producing side effects on repeated runs ❌ Failing silently on subsequent executions


🧠 Template Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Generic idempotent operation template
idempotent_operation() {
    local name="$1"
    local check_cmd="$2"
    local apply_cmd="$3"
    local success_msg="$4"
    local skip_msg="$5"

    if eval "$check_cmd"; then
        echo "$skip_msg"
        return 0
    fi

    if eval "$apply_cmd"; then
        echo "$success_msg"
        return 0
    else
        echo "Failed to $name" >&2
        return 1
    fi
}

# Example usage
idempotent_operation \
    "create directory" \
    "[ -d /app/data ]" \
    "mkdir -p /app/data" \
    "Created directory: /app/data" \
    "Directory already exists: /app/data"

🧾 See Also