specweave 1.0.28 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +39 -0
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/commands/check-hooks.md +43 -0
- package/plugins/specweave/hooks/lib/circuit-breaker.sh +381 -0
- package/plugins/specweave/hooks/lib/logging.sh +231 -0
- package/plugins/specweave/hooks/lib/metrics.sh +347 -0
- package/plugins/specweave/hooks/lib/semaphore.sh +216 -0
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +156 -22
- package/plugins/specweave/scripts/hook-health.sh +441 -0
- package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
- /package/plugins/specweave/hooks/{hooks.json.bak → hooks.json} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# semaphore.sh - File-based semaphore for limiting concurrent hook execution
|
|
3
|
+
#
|
|
4
|
+
# PROBLEM SOLVED:
|
|
5
|
+
# Process storms occur when many hooks spawn simultaneously, overwhelming the system.
|
|
6
|
+
# Instead of detecting storms and blocking EVERYTHING (current behavior), this semaphore
|
|
7
|
+
# limits concurrency properly - excess requests wait or timeout gracefully.
|
|
8
|
+
#
|
|
9
|
+
# USAGE:
|
|
10
|
+
# source semaphore.sh
|
|
11
|
+
# if acquire_semaphore "hook-name" 10 5000; then
|
|
12
|
+
# # Do work
|
|
13
|
+
# release_semaphore "hook-name"
|
|
14
|
+
# else
|
|
15
|
+
# # Timeout - return safe default
|
|
16
|
+
# fi
|
|
17
|
+
#
|
|
18
|
+
# DESIGN:
|
|
19
|
+
# - Uses file-based locks (portable, no dependencies)
|
|
20
|
+
# - Configurable max concurrent slots
|
|
21
|
+
# - Configurable timeout with exponential backoff
|
|
22
|
+
# - Auto-cleanup of stale locks (older than 30s)
|
|
23
|
+
# - Request queuing with FIFO ordering
|
|
24
|
+
#
|
|
25
|
+
# v1.0.0 - Initial implementation (2025-12-17)
|
|
26
|
+
|
|
27
|
+
set -o pipefail
|
|
28
|
+
|
|
29
|
+
# === Configuration ===
|
|
30
|
+
SEMAPHORE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/semaphores"
|
|
31
|
+
SEMAPHORE_MAX_AGE_SECONDS=30 # Stale lock threshold
|
|
32
|
+
SEMAPHORE_DEBUG="${SEMAPHORE_DEBUG:-0}"
|
|
33
|
+
|
|
34
|
+
# === Initialization ===
|
|
35
|
+
_init_semaphore_dir() {
|
|
36
|
+
mkdir -p "$SEMAPHORE_DIR" 2>/dev/null || true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# === Logging ===
|
|
40
|
+
_sem_log() {
|
|
41
|
+
[[ "$SEMAPHORE_DEBUG" == "1" ]] && echo "[SEM $(date +%H:%M:%S.%3N)] $*" >&2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# === Cleanup stale locks ===
|
|
45
|
+
# Locks older than SEMAPHORE_MAX_AGE_SECONDS are considered abandoned
|
|
46
|
+
cleanup_stale_locks() {
|
|
47
|
+
local name="$1"
|
|
48
|
+
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
49
|
+
|
|
50
|
+
[[ ! -d "$lock_dir" ]] && return 0
|
|
51
|
+
|
|
52
|
+
local now
|
|
53
|
+
now=$(date +%s)
|
|
54
|
+
|
|
55
|
+
for lock_file in "$lock_dir"/*.lock; do
|
|
56
|
+
[[ ! -f "$lock_file" ]] && continue
|
|
57
|
+
|
|
58
|
+
local mtime
|
|
59
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
60
|
+
mtime=$(stat -f %m "$lock_file" 2>/dev/null || echo "0")
|
|
61
|
+
else
|
|
62
|
+
mtime=$(stat -c %Y "$lock_file" 2>/dev/null || echo "0")
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
local age=$((now - mtime))
|
|
66
|
+
if [[ "$age" -gt "$SEMAPHORE_MAX_AGE_SECONDS" ]]; then
|
|
67
|
+
_sem_log "Cleaning stale lock: $lock_file (age: ${age}s)"
|
|
68
|
+
rm -f "$lock_file" 2>/dev/null || true
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# === Count active slots ===
|
|
74
|
+
count_active_slots() {
|
|
75
|
+
local name="$1"
|
|
76
|
+
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
77
|
+
|
|
78
|
+
[[ ! -d "$lock_dir" ]] && echo "0" && return
|
|
79
|
+
|
|
80
|
+
local count=0
|
|
81
|
+
for lock_file in "$lock_dir"/*.lock; do
|
|
82
|
+
[[ -f "$lock_file" ]] && count=$((count + 1))
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
echo "$count"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# === Acquire semaphore slot ===
|
|
89
|
+
# Args: name, max_slots, timeout_ms
|
|
90
|
+
# Returns: 0 if acquired, 1 if timeout
|
|
91
|
+
acquire_semaphore() {
|
|
92
|
+
local name="${1:-default}"
|
|
93
|
+
local max_slots="${2:-10}"
|
|
94
|
+
local timeout_ms="${3:-5000}"
|
|
95
|
+
|
|
96
|
+
_init_semaphore_dir
|
|
97
|
+
|
|
98
|
+
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
99
|
+
mkdir -p "$lock_dir" 2>/dev/null || true
|
|
100
|
+
|
|
101
|
+
# Generate unique slot ID (use random instead of %N which doesn't work on macOS)
|
|
102
|
+
local slot_id="$$-$RANDOM$RANDOM"
|
|
103
|
+
local lock_file="$lock_dir/${slot_id}.lock"
|
|
104
|
+
|
|
105
|
+
# Store slot ID for release
|
|
106
|
+
export _SEMAPHORE_SLOT_ID="$slot_id"
|
|
107
|
+
export _SEMAPHORE_NAME="$name"
|
|
108
|
+
export _SEMAPHORE_LOCK_FILE="$lock_file"
|
|
109
|
+
|
|
110
|
+
local start_time
|
|
111
|
+
# macOS doesn't support %N, use seconds * 1000 as approximation
|
|
112
|
+
if command -v gdate &>/dev/null; then
|
|
113
|
+
start_time=$(gdate +%s%3N)
|
|
114
|
+
else
|
|
115
|
+
start_time=$(($(date +%s) * 1000))
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
local attempt=0
|
|
119
|
+
local backoff_ms=10 # Start with 10ms backoff
|
|
120
|
+
local max_backoff_ms=200
|
|
121
|
+
|
|
122
|
+
while true; do
|
|
123
|
+
# Cleanup stale locks periodically (every 5 attempts)
|
|
124
|
+
[[ $((attempt % 5)) -eq 0 ]] && cleanup_stale_locks "$name"
|
|
125
|
+
|
|
126
|
+
local current_slots
|
|
127
|
+
current_slots=$(count_active_slots "$name")
|
|
128
|
+
|
|
129
|
+
if [[ "$current_slots" -lt "$max_slots" ]]; then
|
|
130
|
+
# Try to acquire slot atomically
|
|
131
|
+
if (set -o noclobber; echo "$$" > "$lock_file") 2>/dev/null; then
|
|
132
|
+
_sem_log "Acquired slot $slot_id for $name (slots: $((current_slots + 1))/$max_slots)"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Check timeout
|
|
138
|
+
local now
|
|
139
|
+
if command -v gdate &>/dev/null; then
|
|
140
|
+
now=$(gdate +%s%3N)
|
|
141
|
+
else
|
|
142
|
+
now=$(($(date +%s) * 1000))
|
|
143
|
+
fi
|
|
144
|
+
local elapsed=$((now - start_time))
|
|
145
|
+
|
|
146
|
+
if [[ "$elapsed" -ge "$timeout_ms" ]]; then
|
|
147
|
+
_sem_log "Timeout acquiring $name after ${elapsed}ms (slots: $current_slots/$max_slots)"
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Exponential backoff with jitter
|
|
152
|
+
local jitter=$((RANDOM % 20))
|
|
153
|
+
local sleep_ms=$((backoff_ms + jitter))
|
|
154
|
+
|
|
155
|
+
_sem_log "Waiting for slot $name (attempt $attempt, backoff ${sleep_ms}ms, slots: $current_slots/$max_slots)"
|
|
156
|
+
|
|
157
|
+
# Sleep (convert ms to fractional seconds)
|
|
158
|
+
sleep "0.$(printf '%03d' $sleep_ms)" 2>/dev/null || sleep 0.1
|
|
159
|
+
|
|
160
|
+
# Increase backoff (exponential with cap)
|
|
161
|
+
backoff_ms=$((backoff_ms * 2))
|
|
162
|
+
[[ "$backoff_ms" -gt "$max_backoff_ms" ]] && backoff_ms=$max_backoff_ms
|
|
163
|
+
|
|
164
|
+
attempt=$((attempt + 1))
|
|
165
|
+
done
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# === Release semaphore slot ===
|
|
169
|
+
release_semaphore() {
|
|
170
|
+
local name="${1:-$_SEMAPHORE_NAME}"
|
|
171
|
+
local lock_file="${_SEMAPHORE_LOCK_FILE}"
|
|
172
|
+
|
|
173
|
+
if [[ -n "$lock_file" ]] && [[ -f "$lock_file" ]]; then
|
|
174
|
+
rm -f "$lock_file" 2>/dev/null || true
|
|
175
|
+
_sem_log "Released slot for $name"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
unset _SEMAPHORE_SLOT_ID
|
|
179
|
+
unset _SEMAPHORE_NAME
|
|
180
|
+
unset _SEMAPHORE_LOCK_FILE
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# === Get semaphore status ===
|
|
184
|
+
get_semaphore_status() {
|
|
185
|
+
local name="${1:-default}"
|
|
186
|
+
local max_slots="${2:-10}"
|
|
187
|
+
|
|
188
|
+
_init_semaphore_dir
|
|
189
|
+
cleanup_stale_locks "$name"
|
|
190
|
+
|
|
191
|
+
local active
|
|
192
|
+
active=$(count_active_slots "$name")
|
|
193
|
+
|
|
194
|
+
echo "{\"name\":\"$name\",\"active\":$active,\"max\":$max_slots,\"available\":$((max_slots - active))}"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# === Force release all slots (emergency) ===
|
|
198
|
+
force_release_all() {
|
|
199
|
+
local name="${1:-default}"
|
|
200
|
+
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
201
|
+
|
|
202
|
+
if [[ -d "$lock_dir" ]]; then
|
|
203
|
+
rm -f "$lock_dir"/*.lock 2>/dev/null || true
|
|
204
|
+
_sem_log "Force released all slots for $name"
|
|
205
|
+
fi
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# === Trap handler for automatic cleanup ===
|
|
209
|
+
_semaphore_cleanup_trap() {
|
|
210
|
+
release_semaphore 2>/dev/null || true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Register cleanup trap if sourced
|
|
214
|
+
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
|
|
215
|
+
trap _semaphore_cleanup_trap EXIT INT TERM
|
|
216
|
+
fi
|
|
@@ -1,37 +1,70 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# fail-fast-wrapper.sh - HARD TIMEOUT wrapper for all hooks
|
|
2
|
+
# fail-fast-wrapper.sh - HARD TIMEOUT wrapper for all hooks with proper concurrency control
|
|
3
3
|
# If ANY hook takes longer than HOOK_TIMEOUT, it gets KILLED.
|
|
4
4
|
#
|
|
5
5
|
# Usage: bash fail-fast-wrapper.sh <hook-script> [args...]
|
|
6
6
|
#
|
|
7
7
|
# Environment:
|
|
8
|
-
# HOOK_TIMEOUT
|
|
9
|
-
# HOOK_DEBUG
|
|
8
|
+
# HOOK_TIMEOUT - max seconds (default: 5)
|
|
9
|
+
# HOOK_DEBUG - set to 1 for verbose logging
|
|
10
|
+
# HOOK_MAX_CONCURRENT - max concurrent hooks (default: 15)
|
|
11
|
+
# HOOK_ACQUIRE_TIMEOUT_MS - semaphore acquire timeout (default: 3000)
|
|
10
12
|
#
|
|
11
13
|
# Exit behavior:
|
|
12
14
|
# - Returns hook output on success
|
|
13
15
|
# - Returns safe JSON on timeout ({"continue":true} or {"decision":"approve"})
|
|
14
16
|
# - NEVER hangs - timeout is enforced with SIGKILL
|
|
15
17
|
#
|
|
16
|
-
#
|
|
17
|
-
# -
|
|
18
|
-
# -
|
|
19
|
-
# -
|
|
18
|
+
# CONCURRENCY CONTROL (v1.0.30):
|
|
19
|
+
# - Semaphore-based concurrency limiting (NOT process storm detection)
|
|
20
|
+
# - Proper circuit breaker with CLOSED/OPEN/HALF_OPEN states
|
|
21
|
+
# - Structured logging with request tracing
|
|
22
|
+
# - Metrics collection for observability
|
|
20
23
|
#
|
|
21
24
|
# v0.33.0 - Enhanced with crash prevention integration
|
|
25
|
+
# v1.0.30 - Complete rewrite with proper concurrency primitives
|
|
22
26
|
|
|
23
27
|
set -o pipefail
|
|
24
28
|
|
|
25
29
|
# === Configuration ===
|
|
26
30
|
HOOK_TIMEOUT="${HOOK_TIMEOUT:-5}" # 5 seconds - more than enough for any hook
|
|
27
31
|
HOOK_DEBUG="${HOOK_DEBUG:-0}"
|
|
32
|
+
HOOK_MAX_CONCURRENT="${HOOK_MAX_CONCURRENT:-15}" # Max concurrent hooks
|
|
33
|
+
HOOK_ACQUIRE_TIMEOUT_MS="${HOOK_ACQUIRE_TIMEOUT_MS:-3000}" # 3 seconds to acquire semaphore
|
|
28
34
|
LOG_FILE="${HOME}/.claude/hook-failures.log"
|
|
29
35
|
|
|
30
|
-
# ===
|
|
36
|
+
# === Library paths ===
|
|
31
37
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
-
|
|
38
|
+
LIB_DIR="${SCRIPT_DIR}/../lib"
|
|
33
39
|
|
|
34
|
-
# Source
|
|
40
|
+
# === Source libraries (fail gracefully if missing) ===
|
|
41
|
+
SEMAPHORE_LOADED=false
|
|
42
|
+
CIRCUIT_BREAKER_LOADED=false
|
|
43
|
+
LOGGING_LOADED=false
|
|
44
|
+
METRICS_LOADED=false
|
|
45
|
+
|
|
46
|
+
# Set state dir for all libraries
|
|
47
|
+
export SPECWEAVE_STATE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}"
|
|
48
|
+
export SPECWEAVE_LOG_DIR="${SPECWEAVE_LOG_DIR:-.specweave/logs/hooks}"
|
|
49
|
+
|
|
50
|
+
if [[ -f "$LIB_DIR/semaphore.sh" ]]; then
|
|
51
|
+
source "$LIB_DIR/semaphore.sh" 2>/dev/null && SEMAPHORE_LOADED=true
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ -f "$LIB_DIR/circuit-breaker.sh" ]]; then
|
|
55
|
+
source "$LIB_DIR/circuit-breaker.sh" 2>/dev/null && CIRCUIT_BREAKER_LOADED=true
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ -f "$LIB_DIR/logging.sh" ]]; then
|
|
59
|
+
source "$LIB_DIR/logging.sh" 2>/dev/null && LOGGING_LOADED=true
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [[ -f "$LIB_DIR/metrics.sh" ]]; then
|
|
63
|
+
source "$LIB_DIR/metrics.sh" 2>/dev/null && METRICS_LOADED=true
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Legacy crash prevention (fallback)
|
|
67
|
+
CRASH_PREVENTION="${LIB_DIR}/crash-prevention.sh"
|
|
35
68
|
if [[ -f "$CRASH_PREVENTION" ]]; then
|
|
36
69
|
source "$CRASH_PREVENTION" 2>/dev/null || true
|
|
37
70
|
fi
|
|
@@ -44,7 +77,7 @@ log_debug() {
|
|
|
44
77
|
log_failure() {
|
|
45
78
|
local msg="$1"
|
|
46
79
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
47
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK
|
|
80
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK FAILURE: $msg" >> "$LOG_FILE"
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
# === Safe JSON output based on hook type ===
|
|
@@ -58,6 +91,12 @@ get_safe_output() {
|
|
|
58
91
|
fi
|
|
59
92
|
}
|
|
60
93
|
|
|
94
|
+
# === Get hook name from script path ===
|
|
95
|
+
get_hook_name() {
|
|
96
|
+
local script="$1"
|
|
97
|
+
basename "$script" .sh | tr '/' '-'
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
# === Read stdin with timeout ===
|
|
62
101
|
# Critical: stdin can block forever if not handled properly
|
|
63
102
|
read_stdin_with_timeout() {
|
|
@@ -92,18 +131,68 @@ main() {
|
|
|
92
131
|
exit 0
|
|
93
132
|
fi
|
|
94
133
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
134
|
+
local hook_name
|
|
135
|
+
hook_name=$(get_hook_name "$script")
|
|
136
|
+
|
|
137
|
+
# === Initialize libraries ===
|
|
138
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
139
|
+
log_init "$hook_name"
|
|
140
|
+
log_debug "Starting hook execution" "script=$script"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
144
|
+
metrics_init "$hook_name"
|
|
145
|
+
metrics_start_request
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# === CIRCUIT BREAKER CHECK ===
|
|
149
|
+
# If circuit is open for this hook, fail fast
|
|
150
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
151
|
+
if ! cb_allow_request "$hook_name"; then
|
|
152
|
+
local cb_status
|
|
153
|
+
cb_status=$(cb_get_status "$hook_name")
|
|
154
|
+
log_debug "Circuit breaker OPEN for $hook_name: $cb_status"
|
|
155
|
+
|
|
156
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
157
|
+
log_warn "Circuit breaker open - failing fast" "hook=$hook_name"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
161
|
+
metrics_end_request "skipped"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
get_safe_output "$script"
|
|
165
|
+
exit 0
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# === SEMAPHORE: Acquire concurrency slot ===
|
|
170
|
+
local semaphore_acquired=false
|
|
171
|
+
if [[ "$SEMAPHORE_LOADED" == "true" ]]; then
|
|
172
|
+
if acquire_semaphore "hooks" "$HOOK_MAX_CONCURRENT" "$HOOK_ACQUIRE_TIMEOUT_MS"; then
|
|
173
|
+
semaphore_acquired=true
|
|
174
|
+
log_debug "Acquired semaphore slot for $hook_name"
|
|
175
|
+
else
|
|
176
|
+
# Could not acquire semaphore in time - graceful degradation
|
|
177
|
+
log_debug "Semaphore timeout for $hook_name - graceful skip"
|
|
178
|
+
|
|
179
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
180
|
+
log_warn "Semaphore acquisition timeout - graceful degradation" "hook=$hook_name" "max_concurrent=$HOOK_MAX_CONCURRENT"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
184
|
+
metrics_end_request "skipped"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# DON'T record as failure - this is graceful degradation, not an error
|
|
102
188
|
get_safe_output "$script"
|
|
103
189
|
exit 0
|
|
104
190
|
fi
|
|
105
191
|
fi
|
|
106
192
|
|
|
193
|
+
# Ensure semaphore is released on exit
|
|
194
|
+
trap 'release_semaphore 2>/dev/null || true' EXIT INT TERM
|
|
195
|
+
|
|
107
196
|
log_debug "Executing: $script (timeout: ${HOOK_TIMEOUT}s)"
|
|
108
197
|
|
|
109
198
|
# Read stdin first (with its own timeout)
|
|
@@ -111,11 +200,8 @@ main() {
|
|
|
111
200
|
stdin_content=$(read_stdin_with_timeout)
|
|
112
201
|
|
|
113
202
|
# Execute the hook with hard timeout
|
|
114
|
-
# Using timeout with --kill-after to ensure SIGKILL if SIGTERM doesn't work
|
|
115
203
|
local output
|
|
116
204
|
local exit_code
|
|
117
|
-
|
|
118
|
-
# Create temp file for output (avoid subshell issues)
|
|
119
205
|
local tmp_out
|
|
120
206
|
tmp_out=$(mktemp)
|
|
121
207
|
|
|
@@ -156,12 +242,30 @@ main() {
|
|
|
156
242
|
output=$(cat "$tmp_out" 2>/dev/null)
|
|
157
243
|
rm -f "$tmp_out"
|
|
158
244
|
|
|
245
|
+
# Release semaphore immediately after execution
|
|
246
|
+
if [[ "$semaphore_acquired" == "true" ]]; then
|
|
247
|
+
release_semaphore
|
|
248
|
+
fi
|
|
249
|
+
|
|
159
250
|
# Handle timeout (exit code 124 or 137)
|
|
160
251
|
if [[ $exit_code -eq 124 ]] || [[ $exit_code -eq 137 ]]; then
|
|
161
252
|
log_failure "$script - killed after ${HOOK_TIMEOUT}s"
|
|
162
253
|
log_debug "TIMEOUT: $script killed after ${HOOK_TIMEOUT}s"
|
|
163
254
|
|
|
164
|
-
|
|
255
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
256
|
+
log_error "Hook timeout - killed after ${HOOK_TIMEOUT}s" "hook=$hook_name"
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
260
|
+
metrics_end_request "timeout"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Record failure for circuit breaker
|
|
264
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
265
|
+
cb_record_failure "$hook_name"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# Clean up potential zombie processes (legacy)
|
|
165
269
|
if type kill_zombie_heredocs &>/dev/null; then
|
|
166
270
|
kill_zombie_heredocs 2>/dev/null || true
|
|
167
271
|
fi
|
|
@@ -170,6 +274,36 @@ main() {
|
|
|
170
274
|
exit 0
|
|
171
275
|
fi
|
|
172
276
|
|
|
277
|
+
# Handle hook errors (non-zero exit, excluding block exit code 2)
|
|
278
|
+
if [[ $exit_code -ne 0 ]] && [[ $exit_code -ne 2 ]]; then
|
|
279
|
+
log_debug "Hook error: $script exited with $exit_code"
|
|
280
|
+
|
|
281
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
282
|
+
log_warn "Hook exited with error" "hook=$hook_name" "exit_code=$exit_code"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
286
|
+
metrics_end_request "failure"
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
290
|
+
cb_record_failure "$hook_name"
|
|
291
|
+
fi
|
|
292
|
+
else
|
|
293
|
+
# Success!
|
|
294
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
295
|
+
metrics_end_request "success"
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
299
|
+
cb_record_success "$hook_name"
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
303
|
+
log_debug "Hook completed successfully" "hook=$hook_name"
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
|
|
173
307
|
# Return output or safe default
|
|
174
308
|
if [[ -n "$output" ]]; then
|
|
175
309
|
echo "$output"
|