View source with raw comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        jan@swi-prolog.org
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2020-2024, SWI-Prolog Solutions b.v.
    7    All rights reserved.
    8
    9    Redistribution and use in source and binary forms, with or without
   10    modification, are permitted provided that the following conditions
   11    are met:
   12
   13    1. Redistributions of source code must retain the above copyright
   14       notice, this list of conditions and the following disclaimer.
   15
   16    2. Redistributions in binary form must reproduce the above copyright
   17       notice, this list of conditions and the following disclaimer in
   18       the documentation and/or other materials provided with the
   19       distribution.
   20
   21    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   22    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   23    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   24    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   25    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   26    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   27    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   28    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   29    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   30    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   31    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   32    POSSIBILITY OF SUCH DAMAGE.
   33*/
   34
   35:- module(http_redis_plugin, []).   36:- use_module(library(http/http_session)).   37:- autoload(library(error), [must_be/2]).   38:- autoload(library(lists), [member/2]).   39:- autoload(library(redis), [redis/3, redis/2, redis_get_list/4, redis_zscan/4]).   40:- autoload(library(broadcast), [broadcast/1]).   41:- use_module(library(debug), [debug/3]).   42:- autoload(library(socket), [ip_name/2]).

Hook session management to use Redis

This module acts as a plugin for library(http/http_session), storing session information on a Redis server. This has several consequences:

The library is activated by loading it in addition to library(http/http_session) and using http_set_session_options/1 to configure the Redis database as below. The redis_server/2 predicate from library(redis) can be used to specify the parameters for the redis server such as host, port or authentication.

:- http_set_session_options(
       [ redis_db(default),
         redis_prefix('swipl:http:session')
       ]).

Redis key usage

All Redis keys reside under a prefix specified by the option redis_prefix(Prefix), which defaults to 'swipl:http:session'. Here we find:

   87:- multifile
   88    http_session:hooked/0,
   89    http_session:hook/1,
   90    http_session:session_setting/1,
   91    http_session:session_option/2.   92
   93http_session:session_option(redis_db, atom).
   94http_session:session_option(redis_ro, atom).
   95http_session:session_option(redis_prefix, atom).
   96
   97http_session:hooked :-
   98    http_session:session_setting(redis_db(_)).
   99
  100%http_session:hook(assert_session(SessionID, Peer)).
  101%http_session:hook(set_session_option(SessionId, Setting)).
  102%http_session:hook(get_session_option(SessionId, Setting)).
  103%http_session:hook(active_session(SessionID, Peer, LastUsed)).
  104%http_session:hook(set_last_used(SessionID, Now, TimeOut)).
  105%http_session:hook(asserta(session_data(SessionId, Data))).
  106%http_session:hook(assertz(session_data(SessionId, Data))).
  107%http_session:hook(retract(session_data(SessionId, Data))).
  108%http_session:hook(retractall(session_data(SessionId, Data))).
  109%http_session:hook(session_data(SessionId, Data)).
  110%http_session:hook(current_session(SessionID, Data)).
  111%http_session:hook(close_session(?SessionID)).
  112%http_session:hook(gc_sessions).
  113
  114:- dynamic
  115    peer/2,                             % SessionID, Peer
  116    last_used/2.                        % SessionID, Time
  117
  118
  119http_session:hook(assert_session(SessionID, Peer)) :-
  120    session_db(rw, SessionID, DB, Key),
  121    http_session:session_setting(timeout(Timeout)),
  122    asserta(peer(SessionID, Peer)),
  123    ip_name(Peer, PeerS),
  124    get_time(Now),
  125    redis(DB, hset(Key,
  126                   peer, PeerS,
  127                   last_used, Now)),
  128    expire(SessionID, Timeout).
  129http_session:hook(set_session_option(SessionID, Setting)) :-
  130    session_db(rw, SessionID, DB, Key),
  131    Setting =.. [Name,Value],
  132    redis(DB, hset(Key, Name, Value as prolog)),
  133    (   Setting = timeout(Timeout)
  134    ->  expire(SessionID, Timeout)
  135    ;   true
  136    ).
  137http_session:hook(get_session_option(SessionID, Setting)) :-
  138    session_db(ro, SessionID, DB, Key),
  139    Setting =.. [Name,Value],
  140    redis(DB, hget(Key, Name), Value).
  141http_session:hook(active_session(SessionID, Peer, LastUsed)) :-
  142    (   last_used(SessionID, LastUsed0),
  143        peer(SessionID, Peer0)
  144    ->  LastUsed = LastUsed0,
  145        Peer = Peer0
  146    ;   session_db(ro, SessionID, DB, Key),
  147        redis(DB, hget(Key, peer), PeerS),
  148        ip_name(Peer, PeerS),
  149        redis(DB, hget(Key, last_used), LastUsed as number),
  150        update_session(SessionID, LastUsed, _, Peer)
  151    ).
  152http_session:hook(set_last_used(SessionID, Now, Timeout)) :-
  153    http_session:session_setting(granularity(TimeGranularity)),
  154    LastUsed is floor(Now/TimeGranularity)*TimeGranularity,
  155    update_session(SessionID, LastUsed, Updated, _Peer),
  156    (   Updated == true
  157    ->  session_db(rw, SessionID, DB, Key),
  158        redis(DB, hset(Key, last_used, Now)),
  159        expire(SessionID, Timeout)
  160    ;   true
  161    ).
  162http_session:hook(asserta(session_data(SessionID, Data))) :-
  163    must_be(ground, Data),
  164    session_data_db(rw, SessionID, DB, Key),
  165    redis(DB, lpush(Key, Data as prolog)).
  166http_session:hook(assertz(session_data(SessionID, Data))) :-
  167    must_be(ground, Data),
  168    session_data_db(rw, SessionID, DB, Key),
  169    redis(DB, rpush(Key, Data as prolog)).
  170http_session:hook(retract(session_data(SessionID, Data))) :-
  171    session_data_db(rw, SessionID, DB, Key),
  172    redis_get_list(DB, Key, 10, List),
  173    member(Data, List),
  174    redis(DB, lrem(Key, 1, Data as prolog)).
  175http_session:hook(retractall(session_data(SessionID, Data))) :-
  176    forall(http_session:hook(retract(session_data(SessionID, Data))),
  177           true).
  178http_session:hook(session_data(SessionID, Data)) :-
  179    session_data_db(rw, SessionID, DB, Key),
  180    redis_get_list(DB, Key, 10, List),
  181    member(Data, List).
  182http_session:hook(current_session(SessionID, Data)) :-
  183    session_db(ro, SessionID, DB, Key),
  184    redis(DB, hget(Key, last_used), Time as number),
  185    number(Time),
  186    get_time(Now),
  187    Idle is Now - Time,
  188    (   nonvar(Data)
  189    ->  (   Data = peer(Peer)
  190        ->  redis(DB, hget(Key, peer), PeerS),
  191            ip_name(Peer, PeerS)
  192        ;   Data = idle(Idle0)
  193        ->  Idle0 = Idle
  194        ;   http_session:hook(session_data(SessionID, Data))
  195        )
  196    ;   (   Data = peer(Peer),
  197            redis(DB, hget(Key, peer), PeerS),
  198            ip_name(Peer, PeerS)
  199        ;   Data = idle(Idle)
  200        ;   non_reserved_property(Data),
  201            http_session:hook(session_data(SessionID, Data))
  202        )
  203    ).
  204http_session:hook(close_session(SessionID)) :-
  205    gc_session(SessionID).
  206http_session:hook(gc_sessions) :-
  207    gc_sessions.
  208
  209non_reserved_property(P) :-
  210    var(P),
  211    !.
  212non_reserved_property(peer(_)) :- !, fail.
  213non_reserved_property(idle(_)) :- !, fail.
  214non_reserved_property(_).
 update_session(+SessionID, ?LastUsed, -Updated, ?Peer) is det
Update cached last_used and peer notions.
  221update_session(SessionID, LastUsed, Updated, Peer) :-
  222    transaction(update_session_(SessionID, LastUsed, Updated, Peer)).
  223
  224update_session_(SessionID, LastUsed, Updated, Peer) :-
  225    update_last_used(SessionID, Updated, LastUsed),
  226    update_peer(SessionID, Peer).
  227
  228update_last_used(SessionID, Updated, LastUsed), nonvar(LastUsed) =>
  229    (   last_used(SessionID, LastUsed)
  230    ->  true
  231    ;   retractall(last_used(SessionID, _)),
  232        asserta(last_used(SessionID, LastUsed)),
  233        Updated = true
  234    ).
  235update_last_used(_, _, _) =>
  236    true.
  237
  238update_peer(SessionID, Peer), nonvar(Peer) =>
  239    (   peer(SessionID, Peer)
  240    ->  true
  241    ;   retractall(peer(SessionID, _)),
  242        asserta(peer(SessionID, Peer))
  243    ).
  244update_peer(_, _) =>
  245    true.
  246
  247
  248		 /*******************************
  249		 *      SCHEDULE TIMEOUT	*
  250		 *******************************/
  251
  252expire(SessionID, Timeout) :-
  253    get_time(Now),
  254    Time is integer(Now+Timeout),
  255    session_expire_db(rw, DB, Key),
  256    redis(DB, zadd(Key, Time, SessionID)).
  257
  258gc_sessions :-
  259    session_expire_db(ro, DB, Key),
  260    session_expire_db(rw, DB, TMOKey),
  261    get_time(Now),
  262    End is integer(Now),
  263    redis(DB, zrangebyscore(Key, "-inf", End), TimedOut as atom),
  264    (   member(SessionID, TimedOut),
  265        redis(DB, zrem(TMOKey, SessionID), 1), % 0 if someone else deleted this
  266        gc_session(SessionID),
  267        fail
  268    ;   true
  269    ).
 gc_session(+SessionID) is semidet
  273gc_session(_) :-
  274    prolog_current_frame(Frame),
  275    prolog_frame_attribute(Frame, parent, PFrame),
  276    prolog_frame_attribute(PFrame, parent_goal, gc_session(_)),
  277    !.
  278gc_session(SessionID) :-
  279    debug(http_session(gc), 'GC session ~p', [SessionID]),
  280    session_db(ro, SessionID, DB, SessionKey),
  281    redis(DB, hget(SessionKey, peer), PeerS),
  282    ip_name(Peer, PeerS),
  283    broadcast(http_session(end(SessionID, Peer))),
  284    session_db(rw, SessionID, DBw, SessionKey),
  285    redis(DBw, del(SessionKey)),
  286    session_data_db(rw, SessionID, DBw, DataKey),
  287    redis(DBw, del(DataKey)),
  288    retractall(peer(SessionID, _)),
  289    retractall(last_used(SessionID, _)).
  290
  291
  292		 /*******************************
  293		 *             UTIL		*
  294		 *******************************/
 session_db(+RW, ?SessionID, -DB, -Key) is det
  298session_db(RW, SessionID, DB, Key) :-
  299    nonvar(SessionID),
  300    !,
  301    redis_db(RW, DB),
  302    key_prefix(Prefix),
  303    atomics_to_string([Prefix,session,SessionID], :, Key).
  304session_db(RW, SessionID, DB, Key) :-
  305    session_expire_db(RW, DB, TMOKey),
  306    redis_zscan(DB, TMOKey, Pairs, []),
  307    member(SessionIDS-_Timeout, Pairs),
  308    atom_string(SessionID, SessionIDS),
  309    key_prefix(Prefix),
  310    atomics_to_string([Prefix,session,SessionID], :, Key).
  311
  312session_expire_db(RW, DB, Key) :-
  313    redis_db(RW, DB),
  314    key_prefix(Prefix),
  315    atomics_to_string([Prefix,expire], :, Key).
  316
  317session_data_db(RW, SessionID, DB, Key) :-
  318    redis_db(RW, DB),
  319    key_prefix(Prefix),
  320    atomics_to_string([Prefix,data,SessionID], :, Key).
  321
  322key_prefix(Prefix) :-
  323    http_session:session_setting(redis_prefix(Prefix)),
  324    !.
  325key_prefix('swipl:http:sessions').
  326
  327redis_db(ro, DB) :-
  328    http_session:session_setting(redis_ro(DB0)),
  329    !,
  330    DB = DB0.
  331redis_db(_, DB) :-
  332    http_session:session_setting(redis_db(DB))