#!/bin/sh # # A locking unique sequence generator. V 1.1 # # Use: sequence next DB > the next number in the sequence # # DB is a file to keep track of the current sequence. # Be aware that the file DB.last will be used to backup the # sequence during incrementation. Lock files such as DB.lock # will also be used. # # # To start a new sequence: # # Use: sequence init DB # # ARG # v 1.1 is_number(){ [ "$(($1+0))" = "$1" ] ; } # PID # v1.0 is_pid_alive(){ [ "$(ps "$1"|awk 'NR==2' 2>/dev/null)" != "" ] ; } # # ------------------ Internal Locking ------------------ # file # v 2.0 lock_getPid() { cat "$1.lock" 2>/dev/null ; } # PID file # v 2.0 lock_isPid() { [ "$(lock_getPid "$2")" = "$1" ] ; } # 0 blocked for sure! 1 not-blocked, >1 might be, 2 bad lock, 3 shifting lock_isBlocked() # v 2.0 { # file typeset file="$1" pid pid=$(lock_getPid "$file") || return 1 is_number "$pid" || return 2 is_pid_alive "$pid" && return 1 # Lock may have been a good lock when we first got the PID, # and lock could have been released and pid finished before # we checked to see if it was alive. Check to make sure # lock is still there to avoid signaling a false block. lock_isPid "$pid" "$file" || return 2 return 0 } lock_unblock() # v 2.1 { # PID file typeset pid="$1" file="$2" lck st lck="$file.lock" lock_isBlocked "$file" ; st=$? if [ $st -gt 1 ] ; then echo "Not sure if blocked, sleep, check again">&2 sleep 1 lock_isBlocked "$file" ; st=$? fi if [ $st -eq 1 ] ; then return 1 ; fi # Let's lock the abandoned lock so that we are the # only attempting to delete it! # Carefull, this is recursive lock "$pid" "$lck" || return 2 # Someone may have already delete the abandoned lock and # aquired a new lock before we got a lock on the lock # If we enusre that the lock is still blocked before # deleting it, we know that we are deleting a blocked lock, # not a newly aquired one! lock_isBlocked "$file" ; st=$? if [ $st -eq 1 ] ; then lock_unlock "$pid" "$lck" ; return 3 fi echo " pid:$pid removing abandonned lock by $(lock_getPid "$file") on $file">&2 rm -f "$lck" lock_unlock "$pid" "$lck" } # 50 (40) works well on carrera lock_spin() # v 1.0 { # pid file max typeset pid="$1" file="$2" max="$3" lck cnt=0 lck="$file.lock" set -o noclobber while [ $cnt -lt $max ] ; do (echo "$pid" > "$lck") >/dev/null 2>&1 && return 0 cnt=$(($cnt+1)) ; done return 1 } # ------------------ External API ------------------ # # A simple locking mechanism. # # Provide a PID and a file to lock on. If the process with # the pid exits, it will abandon any locks it holds. # # lock PID file # unlock file # V 2.2 ## Err 99 : Warning Hijacked Lock! Program accordingly! lock_unlock() # v 2.0 { # pid file typeset pid="$1" file="$2" if [ $# -lt 2 ] ; then echo "Error, use: $0 lock_unlock PID file">&2; return 1 fi if lock_isPid "$pid" "$file" ; then rm -f "$file.lock" >/dev/null 2>&1 && return 0 # Clean Unlock echo "Error: cannot unlock $1" >&2; return 3 fi # Bad PID, assume foul play, but do not make worse by # unlocking someone else's lock! echo "Error: our lock (pid-$pid) on $file was hijacked by\ $( lock_getPid "$file")" >&2 return 99 } ## Err 99 : You already own this lock, they are not nestable! lock() # v 2.0 { # pif file typeset pid="$1" file="$2" cnt=0 try=100 spin=50 if [ $# -lt 2 ] ; then echo "Error, use: $0 lock PID file">&2; return 1 fi if is_number "$pid" ; then : ; else echo "Error: pid:$pid is not a valid pid">&2; return 2 fi while [ $cnt -lt $try ] ; do if is_pid_alive "$pid" ; then : ; else echo "Error: pid:$pid is not alive">&2; return 3 fi lock_spin "$pid" "$file" $spin && return 0 if lock_isPid "$pid" "$file" ; then echo "Error: pid:$pid already holds lock $file">&2; return 99 fi lock_unblock "$pid" "$file" cnt=$(($cnt+1)) ; done echo "Error: $pid cannot get lock for $file">&2 return 4 } # ------------------ Tests ------------------ test1() { # sequence_db typeset db="$1" cnt=0 max=20 echo "test PID:$$" >&2 while [ $cnt -lt $max ] ; do "$0" next "$db" || echo "Missed" >&2 cnt=$(($cnt+1)) ; done } test2() { # sequence_db "$0" --call test1 "$1" & "$0" --call test1 "$1" & wait } # ------------------ Sequence Internal ------------------ # Get the latest sequence number (lock assumed) seq_get() # v 1.0 { # sequence_db typeset db="$1" n last="$1.last" n=$(cat "$db" 2>/dev/null) if is_number "$n" ; then echo $n ; return ; fi # Uhoh, bad things happened, try to recover n=$(cat "$last" 2>/dev/null) if is_number "$n" ; then echo "Warning: sequence was corrupted, restoring to $n">&2 echo $n >| "$db" || return 1 seq_get "$db" return fi # Really bad echo "Error: sequence number was lost">&2 exit 127 } # Backup the latest sequence number (lock assumed) seq_backup() # v 1.0 { # sequence_db typeset db="$1" n bak last="$1.last" n=$(seq_get "$db") || return echo $n >| "$last" || return 2 bak=$(cat "$last" 2>/dev/null) || return 3 is_number "$bak" || return 4 [ $n -eq $bak ] || return 5 echo $n } # Increment the latest sequence number (lock assumed) seq_inc() # v 1.0 { # sequence_db typeset db="$1" n last="$1.last" n=$(seq_backup "$db") || return echo "$(($n + 1))" >| "$db" || return 4 n=$(seq_get "$db") || return rm -f "$last" echo "$n" } # ------------------ Sequence API ------------------ # Output a unique number using sequence_db seq_next() # v 1.0 { # sequence_db typeset db="$1" n lock "$$" "$db" || return n=$(seq_inc "$db") || return lock_unlock "$$" "$db" || return 10 echo "$n" } # Start a new sequence using sequence_db seq_init() # v 1.0 { # sequence_db typeset db="$1" n if [ -e "$db" ] ; then echo "Error: cannot init sequence, file $db already exists">&2 return 1 fi echo "0" > "$db" || return } if [ "$1" = "--call" ] ; then shift ; "$@" ; exit ; fi if [ "$1" = "next" ] ; then shift ; seq_next "$@" ; exit ; fi if [ "$1" = "init" ] ; then shift ; seq_init "$@" ; exit ; fi