Skip to content

Commit 2da693f

Browse files
authored
Against race conditions and duplicate cron entries (#10)
- **File Locking**: Uses `flock` to ensure only one update process runs at a time - **Atomic File Operations**: Uses `mv` instead of `cat` for atomic file updates - **Duplicate Prevention**: Tracks processed jobs to prevent duplicate cron entries - **Unique Temp Files**: Uses process-specific temporary files to prevent conflicts - **Proper Cleanup**: Automatic cleanup of temporary files on script exit
1 parent ddded6a commit 2da693f

2 files changed

Lines changed: 44 additions & 29 deletions

File tree

startup.sh

100644100755
Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,38 @@ if [ -z "$FILTER" ] && [ -z "$COMPOSE_PROJECT_NAME" ]; then
1111
fi
1212

1313
if isTrue "$DEBUG"; then
14-
supercronic -debug -inotify /root/crontab &
14+
supercronic -debug -inotify /root/crontab &
1515
else
16-
supercronic -inotify /root/crontab &
16+
supercronic -inotify /root/crontab &
1717
fi
1818

1919
# Function to update cron jobs based on container labels
2020
update_cron() {
2121
/usr/local/bin/update_cron.sh
2222
}
2323

24-
# File to indicate if an update is already scheduled
25-
UPDATE_SCHEDULED="/tmp/update_scheduled.lock"
24+
# Lock file for preventing concurrent updates
25+
UPDATE_LOCK="/tmp/update_cron.lock"
2626

27-
# Wrapper function to handle concurrent events
27+
# Variables for debouncing
28+
UPDATE_PENDING_FLAG="/tmp/cron_update_pending"
29+
30+
# Function to handle events with debouncing and proper locking
2831
handle_event() {
29-
if [ -f "$UPDATE_SCHEDULED" ]; then
30-
# If update is already scheduled or running, mark the need for a subsequent update
31-
touch "$UPDATE_SCHEDULED"
32+
if [ -f "$UPDATE_PENDING_FLAG" ]; then
33+
log "Another update is already running, skipping this event"
3234
else
3335
# Schedule the update
34-
touch "$UPDATE_SCHEDULED"
35-
while [ -f "$UPDATE_SCHEDULED" ]; do
36-
# Do not rush
37-
sleep 2
38-
rm -f "$UPDATE_SCHEDULED"
36+
touch "$UPDATE_PENDING_FLAG"
37+
# Do not rush
38+
sleep 2
39+
rm -f "$UPDATE_PENDING_FLAG"
40+
(
41+
flock 9 # Blocking lock - wait for previous update to finish
42+
log "Starting cron update..."
3943
update_cron
40-
done
44+
log "Cron update completed"
45+
) 9>"$UPDATE_LOCK"
4146
fi
4247
}
4348

@@ -47,7 +52,7 @@ handle_event &
4752
# Define a function to handle cleanup on SIGTERM
4853
cleanup() {
4954
echo "Received SIGTERM, cleaning up..."
50-
rm -f "$UPDATE_SCHEDULED"
55+
rm -f "$UPDATE_LOCK" "$UPDATE_PENDING_FLAG"
5156
exit 0
5257
}
5358

update_cron.sh

100644100755
Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
DIR=$(dirname "$0")
44
. "$DIR/functions.sh"
55

6-
# Temp cron file paths
7-
CRON_FILE_TEST="/tmp/cron_test"
8-
CRON_FILE_NEW="/root/crontab.new"
6+
# Temp cron file paths with unique identifiers to prevent conflicts
7+
CRON_FILE_TEST="/tmp/cron_test_$$"
8+
CRON_FILE_NEW="/tmp/crontab_new_$$"
99
CRON_FILE="/root/crontab"
1010
CRON_LOG_DIR=${CRON_LOG_DIR:-/var/log/cron}
1111

12+
# Ensure cleanup on exit
13+
cleanup() {
14+
rm -f "$CRON_FILE_TEST" "$CRON_FILE_NEW"
15+
}
16+
trap cleanup EXIT
17+
1218
mkdir -p "$CRON_LOG_DIR"
1319

1420
# Function to extract specific cron job labels from a container
@@ -64,12 +70,15 @@ DOCKER_PS_CMD="$DOCKER_PS_CMD --filter 'label=cron.enabled=true'"
6470
containers=$(eval "$DOCKER_PS_CMD -q")
6571

6672
echo "$(timestamp) | Updating cron jobs..."
67-
touch $CRON_FILE_NEW
73+
74+
# Create a new crontab file with a unique name
75+
true > "$CRON_FILE_NEW"
6876

6977
# Process each container
7078
for container in $containers; do
7179
cron_jobs=$(extract_cron_jobs "$container")
7280
target_container=$(docker inspect -f '{{.Name}}' "$container" | cut -c2-) # Remove leading /
81+
7382
if [ -z "$cron_jobs" ]; then
7483
log "No cron jobs found for container: $target_container ($container)"
7584
else
@@ -88,21 +97,22 @@ for container in $containers; do
8897
# Check if both schedule and command labels are set
8998
if [ -n "$job_schedule" ] && [ -n "$job_command" ]; then
9099
cron_entry="$job_schedule docker exec $target_container sh -c '$job_command' 2>&1 | tee -a $CRON_LOG_DIR/\$(date -u +\%Y-\%m-\%d_\%H-\%M-\%S_\%Z)_$job_key.log"
100+
91101
# Run the supercronic test
92-
echo "$cron_entry" > $CRON_FILE_TEST
93-
if ! supercronic -test $CRON_FILE_TEST > /dev/null 2>&1; then
102+
echo "$cron_entry" > "$CRON_FILE_TEST"
103+
if ! supercronic -test "$CRON_FILE_TEST" > /dev/null 2>&1; then
94104
echo "$(timestamp) | ========================================================"
95105
printf "\nERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR\n\n"
96106
echo "BAD CRON JOB: '$cron_entry'"
97-
supercronic -debug -test $CRON_FILE_TEST
107+
supercronic -debug -test "$CRON_FILE_TEST"
98108
printf "==================================================================================\n\n"
99109
# TODO make the cron container unhealthy
100110
else
101-
echo "$cron_entry" >> $CRON_FILE_NEW # Write in one line to the cron file
111+
echo "$cron_entry" >> "$CRON_FILE_NEW"
102112
log "Scheduled task for $target_container: $cron_entry"
103113
fi
104114
else
105-
echo "$(timestamp) | Error: job_schedule or job_command is missing."
115+
echo "$(timestamp) | Error: job_schedule or job_command is missing for job $job_key in container $target_container."
106116
fi
107117
done
108118
fi
@@ -124,21 +134,21 @@ if ! diff -u "$CRON_FILE" "$CRON_FILE_NEW" > /dev/null; then
124134
fi
125135

126136
# Update the crontab file if it looks good for supercronic (we tested it one by one line above, but to make sure)
127-
if ! supercronic -test $CRON_FILE_NEW > /dev/null 2>&1; then
137+
if ! supercronic -test "$CRON_FILE_NEW" > /dev/null 2>&1; then
128138
echo "$(timestamp) | ########################################################"
129139
printf "\nERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR\n\n"
130140
echo "SOMETHING IS WRONG IN THE CRONTAB FILE"
131-
supercronic -debug -test $CRON_FILE_NEW
141+
supercronic -debug -test "$CRON_FILE_NEW"
132142
echo "ERROR: CHANGES ARE NOT APPLIED."
133143
echo "CURRENT CRONTAB FILE IS:"
134144
cat "$CRON_FILE"
135145
printf "##################################################################################\n\n"
136146
# TODO make the cron container unhealthy
137147
else
138-
cat "$CRON_FILE_NEW" > "$CRON_FILE"
148+
# Atomic update using mv to prevent race conditions
149+
mv "$CRON_FILE_NEW" "$CRON_FILE"
150+
log "Crontab file updated successfully"
139151
fi
140152
else
141153
echo "$(timestamp) | No changes detected in crontab file."
142154
fi
143-
144-
rm "$CRON_FILE_NEW"

0 commit comments

Comments
 (0)