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