unit kern_rangelock;

{$mode ObjFPC}{$H+}
{$CALLING SysV_ABI_CDecl}

interface

uses
 mqueue,
 kern_thr,
 systm,
 kern_mtx;

const
 RL_LOCK_READ     =$0001;
 RL_LOCK_WRITE    =$0002;
 RL_LOCK_TYPE_MASK=$0003;
 RL_LOCK_GRANTED  =$0004;
 RL_LOCK_TRYLOCK  =$0008;

type
 off_t=Int64;

 p_rl_q_entry=^rl_q_entry;

 TAILQ_HEAD=packed record
  tqh_first:p_rl_q_entry;
  tqh_last :^p_rl_q_entry;
 end;

 TAILQ_ENTRY=packed record
  tqe_next:p_rl_q_entry;
  tqe_prev:^p_rl_q_entry;
 end;

 rl_q_entry=record
  rl_q_link :TAILQ_ENTRY; //rl_q_entry
  rl_q_start:off_t;
  rl_q___end:off_t;
  rl_q_flags:Integer;
  rl_q_count:Integer;
  rl_q_td   :p_kthread;
 end;

 p_rangelock=^rangelock;
 rangelock=record
  rl_waiters:TAILQ_HEAD; //rl_q_entry
  rl_currdep:p_rl_q_entry;
 end;

procedure rangelock_init        (lock:p_rangelock);
procedure rangelock_destroy     (lock:p_rangelock);

procedure rangelock_unlock      (lock:p_rangelock;cookie:Pointer;ilk:p_mtx);
function  rangelock_unlock_range(lock:p_rangelock;cookie:Pointer;start,__end:off_t;ilk:p_mtx):Pointer;

function  rangelock_enqueue     (lock:p_rangelock;start,__end:off_t;mode:Integer;ilk:p_mtx):Pointer;

function  rangelock_update      (lock:p_rangelock;start,__end:off_t;mode:Integer;ilk:p_mtx;cookie:Pointer):Boolean;

function  rangelock_rlock       (lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
function  rangelock_tryrlock    (lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
function  rangelock_wlock       (lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
function  rangelock_trywlock    (lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;

procedure rlqentry_free         (rleq:p_rl_q_entry);

implementation

var
 global_rlqe:Pointer=nil;

function rlqentry_alloc():p_rl_q_entry; inline;
begin
 Result:=System.InterlockedExchange(global_rlqe,nil);
 if (Result=nil) then
 begin
  Result:=AllocMem(SizeOf(rl_q_entry));
 end;
end;

procedure rlqentry_free(rleq:p_rl_q_entry); inline;
begin
 if (rleq<>nil) then
 begin
  rleq:=System.InterlockedExchange(global_rlqe,rleq);
  if (rleq<>nil) then
  begin
   FreeMem(rleq);
  end;
 end;
end;

procedure rlqentry_rel(entry:p_rl_q_entry); inline;
begin
 with curkthread^ do
 if (td_rlqe=nil) then
 begin
  td_rlqe:=entry;
 end else
 begin
  rlqentry_free(entry);
 end;
end;

function rlqentry_new():p_rl_q_entry; inline;
begin
 with curkthread^ do
  if (td_rlqe<>nil) then
  begin
   Result:=td_rlqe;
   td_rlqe:=nil;
   Result^:=Default(rl_q_entry);
  end else
  begin
   Result:=rlqentry_alloc();
  end;
end;

procedure rangelock_init(lock:p_rangelock);
begin
 TAILQ_INIT(@lock^.rl_waiters);
 lock^.rl_currdep:=nil;
end;

procedure rangelock_destroy(lock:p_rangelock);
begin
 Assert(TAILQ_EMPTY(@lock^.rl_waiters), 'Dangling waiters');
end;

{
 * Two entries are compatible if their ranges do not overlap, or both
 * entries are for read.
}
function ranges_overlap(e1,e2:p_rl_q_entry):Boolean;
begin
 Result:=(e1^.rl_q_start < e2^.rl_q___end) and (e1^.rl_q___end > e2^.rl_q_start);
end;

{
 * Recalculate the lock^.rl_currdep after an unlock.
}
procedure rangelock_calc_block(lock:p_rangelock);
label
 _out;
var
 entry,nextentry,entry1:p_rl_q_entry;
begin

 //for
 entry:=lock^.rl_currdep;
 while (entry<>nil) do
 begin
  nextentry:=TAILQ_NEXT(entry, @entry^.rl_q_link);

  if (entry^.rl_q_flags and RL_LOCK_READ)<>0 then
  begin
   // Reads must not overlap with granted writes.

   //for
   entry1:=TAILQ_FIRST(@lock^.rl_waiters);
   while ((entry1^.rl_q_flags and RL_LOCK_READ)=0) do
   begin
    if ranges_overlap(entry, entry1) then
    begin
     goto _out;
    end;
    //
    entry1:=TAILQ_NEXT(entry1, @entry1^.rl_q_link);
   end;
   //for

  end else
  begin
   // Write must not overlap with any granted locks.

   //for
   entry1:=TAILQ_FIRST(@lock^.rl_waiters);
   while (entry1<>entry) do
   begin
    if ranges_overlap(entry, entry1) then
    begin
     {
     Writeln('ranges_overlap:',
              HexStr(entry),',',
              HexStr(entry^.rl_q_start,11),',',
              HexStr(entry^.rl_q_end,11),
              ' to ',
              HexStr(entry1),',',
              HexStr(entry1^.rl_q_start,11),',',
              HexStr(entry1^.rl_q_end,11));
     }
     goto _out;
    end;
    //
    entry1:=TAILQ_NEXT(entry1, @entry1^.rl_q_link);
   end;
   //for

   // Move grantable write locks to the front.
   TAILQ_REMOVE     (@lock^.rl_waiters, entry, @entry^.rl_q_link);
   TAILQ_INSERT_HEAD(@lock^.rl_waiters, entry, @entry^.rl_q_link);
  end;

  // Grant this lock.
  entry^.rl_q_flags:=entry^.rl_q_flags or RL_LOCK_GRANTED;
  wakeup(entry);
  //Writeln('rl_wakeup:',HexStr(entry));

  //
  entry:=nextentry;
 end;
 //for

 _out:
  lock^.rl_currdep:=entry;
end;

procedure rangelock_unlock_locked(lock:p_rangelock;entry:p_rl_q_entry;ilk:p_mtx;do_calc_block:Boolean);
begin
 Assert((lock<>nil) and (entry<>nil) and (ilk<>nil));
 mtx_assert(ilk^);

 if (not do_calc_block) then
 begin
  {
   * This is the case where rangelock_enqueue() has been called
   * with trylock=true and just inserted this entry in the
   * queue.
   * If rl_currdep is this entry, rl_currdep needs to
   * be set to the next entry in the rl_waiters list.
   * However, since this entry is the last entry in the
   * list, the next entry is NULL.
  }

  if (lock^.rl_currdep=entry) then
  begin
   Assert(TAILQ_NEXT(lock^.rl_currdep, @lock^.rl_currdep^.rl_q_link)=nil, 'rangelock_enqueue: next entry not NULL');

   lock^.rl_currdep:=nil;
  end;
 end else
 begin
  Assert(entry<>lock^.rl_currdep, 'stuck currdep');
 end;

 TAILQ_REMOVE(@lock^.rl_waiters, entry, @entry^.rl_q_link);
 entry^.rl_q_link:=Default(TAILQ_ENTRY);

 if (do_calc_block) then
 begin
  rangelock_calc_block(lock);
 end;

 mtx_unlock(ilk^);

 rlqentry_rel(entry);
end;

procedure rangelock_unlock(lock:p_rangelock;cookie:Pointer;ilk:p_mtx);
begin
 Assert((lock<>nil) and (cookie<>nil) and (ilk<>nil));

 mtx_lock(ilk^);
 rangelock_unlock_locked(lock, cookie, ilk, true);
end;

{
 * Unlock the sub-range of granted lock.
}
function rangelock_unlock_range(lock:p_rangelock;cookie:Pointer;start,__end:off_t;ilk:p_mtx):Pointer;
var
 entry:p_rl_q_entry;
begin
 Assert((lock<>nil) and (cookie<>nil) and (ilk<>nil));

 entry:=cookie;

 Assert((entry^.rl_q_flags and RL_LOCK_GRANTED)<>0,'Unlocking non-granted lock');
 Assert(entry^.rl_q_start=start , 'wrong start');
 Assert(entry^.rl_q___end>=__end, 'wrong end');

 mtx_lock(ilk^);

 if (entry^.rl_q___end=__end) then
 begin
  rangelock_unlock_locked(lock, cookie, ilk, true);
  Exit(nil);
 end;

 entry^.rl_q___end:=__end;
 rangelock_calc_block(lock);
 mtx_unlock(ilk^);
 Exit(cookie);
end;

{
* Add the lock request to the queue of the pending requests for
* rangelock.  Sleep until the request can be granted unless trylock=true.
}
function rangelock_enqueue(lock:p_rangelock;start,__end:off_t;mode:Integer;ilk:p_mtx):Pointer;
var
 entry:p_rl_q_entry;
begin
 Assert((lock<>nil) and (ilk<>nil));

 entry:=rlqentry_new();
 //
 Assert(entry<>nil);

 entry^.rl_q_flags:=mode;
 entry^.rl_q_start:=start;
 entry^.rl_q___end:=__end;
 entry^.rl_q_td   :=curkthread;

 mtx_lock(ilk^);

 {
  * XXXKIB TODO. Check that a thread does not try to enqueue a
  * lock that is incompatible with another request from the same
  * thread.
 }

 TAILQ_INSERT_TAIL(@lock^.rl_waiters, entry, @entry^.rl_q_link);

 {
  * If rl_currdep=NULL, there is no entry waiting for a conflicting
  * range to be resolved, so set rl_currdep to this entry.  If there is
  * no conflicting entry for this entry, rl_currdep will be set back to
  * NULL by rangelock_calc_block().
 }

 if (lock^.rl_currdep=nil) then
 begin
  lock^.rl_currdep:=entry;
 end;

 rangelock_calc_block(lock);

 while ((entry^.rl_q_flags and RL_LOCK_GRANTED)=0) do
 begin

  if (mode and RL_LOCK_TRYLOCK)<>0 then
  begin
   {
    * For this case, the range is not actually locked
    * yet, but removal from the list requires the same
    * steps, except for not doing a rangelock_calc_block()
    * call, since rangelock_calc_block() was called above.
   }
   rangelock_unlock_locked(lock, entry, ilk, false);
   Exit(nil);
  end;

  //Writeln('rl_msleep:',HexStr(entry));
  msleep(entry, ilk, 0, 'range', 0);
 end;

 mtx_unlock(ilk^);
 Exit(entry);
end;

function get_flags_str(flags:Integer):RawByteString;
const
 _R:array[0..1] of AnsiChar='_R';
 _W:array[0..1] of AnsiChar='_W';
 _T:array[0..1] of AnsiChar='_T';
 _G:array[0..1] of AnsiChar='_G';
begin
 Result:=_R[ord((RL_LOCK_READ      and flags)<>0)]+
         _W[ord((RL_LOCK_WRITE     and flags)<>0)]+
         _G[ord((RL_LOCK_GRANTED   and flags)<>0)]+
         _T[ord((RL_LOCK_TRYLOCK   and flags)<>0)];
end;

function rangelock_update(lock:p_rangelock;start,__end:off_t;mode:Integer;ilk:p_mtx;cookie:Pointer):Boolean;
var
 entry:p_rl_q_entry;

 prev_start:off_t;
 prev___end:off_t;
 prev_flags:Integer;
begin
 Assert((lock<>nil) and (ilk<>nil) and (cookie<>nil));

 entry:=cookie;

 Assert(entry^.rl_q_td=curkthread);

 mtx_lock(ilk^);

 //save
 prev_start:=entry^.rl_q_start;
 prev___end:=entry^.rl_q___end;
 prev_flags:=entry^.rl_q_flags;

 {
 Writeln('rangelock_update:',HexStr(prev_start,16),',',
                             HexStr(prev___end,16),',',
                             get_flags_str(prev_flags),'->',
                             HexStr(start,16),',',
                             HexStr(__end,16),',',
                             get_flags_str(mode)
        );
 }

 //remove range
 TAILQ_REMOVE(@lock^.rl_waiters, entry, @entry^.rl_q_link);
 entry^.rl_q_link:=Default(TAILQ_ENTRY);

 //set new info
 entry^.rl_q_start:=start;
 entry^.rl_q___end:=__end;
 entry^.rl_q_flags:=mode;

 //insert
 TAILQ_INSERT_TAIL(@lock^.rl_waiters, entry, @entry^.rl_q_link);

 {
  * If rl_currdep=NULL, there is no entry waiting for a conflicting
  * range to be resolved, so set rl_currdep to this entry.  If there is
  * no conflicting entry for this entry, rl_currdep will be set back to
  * NULL by rangelock_calc_block().
 }

 if (lock^.rl_currdep=nil) then
 begin
  lock^.rl_currdep:=entry;
 end;

 rangelock_calc_block(lock);

 while ((entry^.rl_q_flags and RL_LOCK_GRANTED)=0) do
 begin

  if (mode and RL_LOCK_TRYLOCK)<>0 then
  begin

   if (lock^.rl_currdep=entry) then
   begin
    lock^.rl_currdep:=nil;
   end;

   //remove range
   TAILQ_REMOVE(@lock^.rl_waiters, entry, @entry^.rl_q_link);
   entry^.rl_q_link:=Default(TAILQ_ENTRY);

   //restore
   entry^.rl_q_start:=prev_start;
   entry^.rl_q___end:=prev___end;
   entry^.rl_q_flags:=prev_flags;

   //insert
   TAILQ_INSERT_TAIL(@lock^.rl_waiters, entry, @entry^.rl_q_link);

   mtx_unlock(ilk^);
   Exit(False);
  end;

  //Writeln('rl_msleep:',HexStr(entry));
  msleep(entry, ilk, 0, 'range', 0);
 end;

 mtx_unlock(ilk^);
 Exit(True);
end;

function rangelock_rlock(lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
begin
 Result:=rangelock_enqueue(lock, start, __end, RL_LOCK_READ, ilk);
end;

function rangelock_tryrlock(lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
begin
 Result:=rangelock_enqueue(lock, start, __end, RL_LOCK_READ or RL_LOCK_TRYLOCK, ilk);
end;

function rangelock_wlock(lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
begin
 Result:=rangelock_enqueue(lock, start, __end, RL_LOCK_WRITE, ilk);
end;

function rangelock_trywlock(lock:p_rangelock;start,__end:off_t;ilk:p_mtx):Pointer;
begin
 Result:=rangelock_enqueue(lock, start, __end, RL_LOCK_WRITE or RL_LOCK_TRYLOCK, ilk);
end;


end.




