ZDOC_3020

Про Shared Memory Objects и немного про память, SHMA, SHMM

В обычной жизни мы не паримся с памятью в ABAP, сборщика мусора как бы нет, глобальной памяти нет.

В самом простом случае у нас есть то, что называется транзакцией (не путать с транзакцией базы данных), что можно определить как “приложение” на нашем собственно говоря сервере приложений. Транзакция всегда ассоциирована с определённой именованной программой.

И вот пока приложение работает, оно потребляет память в виде переменных, экземпляров классов, массивов (также известных как “внутренние таблицы”). И если посмотреть на стек вызовов, то он упирается во входную точку начала этой программы. Ничего ABAPовского за пределами этой точки не существует. Одно приложение не может получить доступ к данным другого приложения, только через какие-то обходные манёвры, например: через постоянное хранилище (СУБД), через взаимные блокировки и прочие костыли. Как только приложение завершает работу, вся его память высвобождается. А приложения долго не работают, потому что они уже в некоторой степени атомарны (то есть как бы уже не монолит, но ещё не микросервисы).

Есть уже случае чуть посложнее, например: есть веб-сервис, слушающий входящие соединения, или одно “приложение” вызывает другое “приложение” через SUBMIT REPORT . Но подход вы уже уловили, это и называется SAP LUW, то есть Logical Unit of Work. Получается, что стек вызовов и контекст существуют неразрывно только внутри него.

И как обычно бывает, изредка появляется необходимость перепрыгнуть заборы.

Один из известных костылей -оператор IMPORT/EXPORT MEMORY ID,  который позволяет перебрасывать переменные (структуры, массивы) между приложениями, но только если они существуют в одной пользовательской сессии. Например Приложение-А вызывает Приложение-Б, Приложение-Б генерирует данные, закидывает их в ячейку памяти X, после чего завершает работу, управление возвращается в Приложение-А, которое считывает данные из ячейки памяти X, профит!

Разговор был бы не так интересен, если бы мы остановились на этом. Но перепрыгнув забор, мы обнаруживаем ещё один забор!

А если мы хотим перебросить данные между разными пользовательскими сессиями?

Для примера кейс будет таким: есть чистый слушающий HTTP-сервис, в котором мы хотим организовать что-то вроде сессии.

HTTP-сессии в ABAP могут быть как stateless, так и stateful. По сути их отличие в том, что этот пресловутый SAP LUW растягивается на всю пользовательскую сессию (stateful) или только на один http-вызов (stateless). В stateful-режиме сервис работает со своими причудами, например: обновлённый код сервиса не подхватывается на лету, чтобы изменения в сервисе вступили в силу нужно завершить сессию. Так же и в обычном ABAPe: пока запущена программа со всеми диалоговыми шагами обновлённый код не подхватывается – необходимо перезайти в транзакцию. Получается в рамках HTTP необходимо сделать log-off, что очень редко случается целенаправленно в жизненных реалиях, гораздо чаще сессия умирает по таймауту.

И вот кривая дорожка сомнительных архитектурных решений привела нас к небольшому челенджу – организовать хранение данных сессии при stateless-сессии HTTP.

Окей, вызов принят. Представим типичный сценарий:

  • Пользователь первый раз заходит на страницу => нет куки сессии => выставляем новый случайный куки (надо его куда-то записать) => рендерим страницу с формой для пользователя => Всё закончилось и очистилось, развязка сезона
  • Ура новый сезон => Пользователь что-то сабмитит => открывается другая страница => есть куки сессии => (надо значения сессии достать) => проверить что пользователь насабмиттил => (положить насабмитенное в сессию) => отрендерить страницу для пользователя, что насабмитилось успешно => снова апокалипсис, миру конец
  • Пользователь чего-то тыкнул => ещё одна страница => есть куки сессии => (надо значения сессии достать) => отрендерить значения на странице => и непонятно, продлит ли пользователь этот бесконечный сериал на следующий сезон или нет

Обыкновенным ABAPом красные места можно тупо реализовать через БД или другой как-мы говорим-у нас-в-деревне “персистент сторэдж”, мы же это умеем. Но мы же дошли досюда не за простыми решениями.

Существует возможность создать экземпляр объекта и положить его в какую-то суперглобальную область памяти, существующую вечно внутри сервера приложений. Соответственно, в любой момент можно попробовать взять этот объект во временное пользование (на чтение или на изменение). И это будет тот-же-самый-объект со всеми его атрибутами. Он будет существовать до перезагрузки сервера или до его ручного уничтожения через кнопку в админке (SHMM). Вот вкратце история. А под катом немного подробностей.

Шаг первый

Сначала создадим сам объект, который будем помещать в память. В местной терминологии он будет называться “корневой/root”.

class ZCL_SESSION definition
  public
  final
  create public
  shared memory enabled .

public section.

  interfaces IF_SHM_BUILD_INSTANCE .

  methods SESSION_GET
    importing
      !IV_SESSION_ID type CHAR32
    exporting
      !ES_SESSION type ZSS_CA_DOC_SESSION .

  methods SESSION_SET
    importing
      !IV_SESSION_ID type CHAR32
      !IV_MYVAL type STRING .

protected section.

private section.

  data MT_SESSIONS type ZTT_CA_DOC_SESSION .

  methods CREATE_ID
    returning
      value(RV_ID) type CHAR32 .

ENDCLASS.

От обычных классов два отличия: включённый флаг SHARED MEMORY (поддержка общей памяти) и используемый интерфейс IF_SHM_BUILD_INSTANCE

Сделаем простую реализацию:

CLASS ZCL_SESSION IMPLEMENTATION.

  METHOD create_id.

    DATA lv_string TYPE string.

    CALL FUNCTION 'GENERAL_GET_RANDOM_STRING'
      EXPORTING
        number_chars  = 32
      IMPORTING
        random_string = lv_string.

    rv_id = lv_string.

  ENDMETHOD.

  METHOD SESSION_GET.

    DATA lv_now TYPE timestamp.

    GET TIME STAMP FIELD lv_now.

    DATA(lv_now_plus_expiration) = cl_abap_tstmp=>add(
        tstmp = lv_now
        secs  = ( 30 * 60 ) "30 минут по 60 секунд
    ).

    IF iv_session_id IS INITIAL.
      APPEND INITIAL LINE TO mt_sessions ASSIGNING FIELD-SYMBOL(<session>).
      <session>-session_id = create_id( ).
      <session>-myval      = space.
    ELSE.
      READ TABLE mt_sessions WITH KEY session_id = iv_session_id ASSIGNING <session>.
      IF sy-subrc = 0.
        IF lv_now  > <session>-expiration.
          <session>-session_id = create_id( ).
          <session>-myval      = space.
        ELSE.
          "как раз старая сессия
        ENDIF.
      ELSE.
        APPEND INITIAL LINE TO mt_sessions ASSIGNING <session>.
        <session>-session_id = create_id( ).
        <session>-myval      = space.
      ENDIF.
    ENDIF.

    <session>-last_activity = lv_now.
    <session>-expiration    = lv_now_plus_expiration.

    es_session = <session>.

  ENDMETHOD.

  METHOD SESSION_SET.

    READ TABLE mt_sessions WITH KEY session_id = iv_session_id
      ASSIGNING FIELD-SYMBOL(<session>).
    IF sy-subrc = 0.
      <session>-myval = iv_myval.
    ENDIF.

  ENDMETHOD.

ENDCLASS.

Ничего сложного, есть айдишка сессии, берём-кладём значения, замечаем отметку время, когда сессия должна ликвидироваться.

Шаг второй

В реализацию метода интерфейса сразу добавим вызов конструктора, чтобы объект всегда существовал, хотя бы пустой для начала:

  METHOD if_shm_build_instance~build.

    DATA: sh_area TYPE REF TO zcl_session_shma,
          sh_root TYPE REF TO zcl_session.

    sh_area = zcl_session_shma=>attach_for_write( ).

    CREATE OBJECT sh_root AREA HANDLE sh_area.

    sh_area->set_root( sh_root ).

    sh_area->detach_commit( ).

  ENDMETHOD.

 

Шаг третий

Теперь в транзакции SHMA необходимо создать “область/area”, для примера с именем ZCL_SESSION_SHMA. При этом будет сгенерирован ещё один класс с именем области. Там не сложно, но придётся указать немного свойств (корневой класс, срок хранения).

Шаг четвёртый

Напрямую кодить  эти объекты куда надо я бы не стал, поэтому нам понадобится пара обвесок:

Для того, чтобы достать сессию: метод GET с параметрами повторяющими метод SESSION_GET, чтобы скрыть внутреннюю кухню:

  METHOD get.

    DATA: sh_area TYPE REF TO zcl_session_shma,
          sh_root TYPE REF TO zcl_session.

    DO.

      TRY.
          sh_area = zcl_session_shma=>attach_for_update( ).
          EXIT.
        CATCH  cx_shm_change_lock_active.
          WAIT UP TO 1 SECONDS.
        CATCH cx_shm_exclusive_lock_active.
          WAIT UP TO 1 SECONDS.
        CATCH cx_shm_no_active_version.
          zcl_session=>if_shm_build_instance~build( ).
      ENDTRY.
    ENDDO.

    sh_root ?= sh_area->get_root( ).

    sh_root->session_get( EXPORTING iv_session_id = iv_session_id
                          IMPORTING es_session = es_session  ).


    sh_area->set_root( sh_root ).

    sh_area->detach_commit( ).

  ENDMETHOD.

И для того чтобы записать сессию почти что аналогично:

  method SET.

    DATA: sh_area TYPE REF TO zcl_session_shma,
          sh_root TYPE REF TO zcl_session.

    DO.

      TRY.
          sh_area = zcl_session_shma=>attach_for_update( ).
          EXIT.
        CATCH  cx_shm_change_lock_active.
          WAIT UP TO 1 SECONDS.
      ENDTRY.
    ENDDO.

    sh_root ?= sh_area->get_root( ).

    sh_root->session_set( EXPORTING iv_session_id = iv_session_id
                                    iv_myval      = iv_myval  ).

    sh_area->set_root( sh_root ).

    sh_area->detach_commit( ).

  endmethod.

Теоретически может произойти и на практике обязательно произойдёт блокировка, если кто-то уже держит объект на запись.

Конечно при определённых условиях оно может встрять надолго,  всё-таки это не очень устойчивый сценарий – бесконечно пробовать раз в секунду пока не получится.

И уже эти два метода (GET и SET)  можно вызывать из нашего исходного кода.

Шаг пятый

Просмотр сессий и запуск инвалидации протухших сессий – задача вне данного proof-of-concept. А без этого понятно, что список сессий будет расти бесконечно, пока не настигнет нас CX_SHM_OUT_OF_MEMORY.

Стоит отметить

В некоторых источниках говорится, что данный подход в таком контексте совсем не рекомендуется:

General shared memory programming is not possible. The current lock logic does not enable you to set specific locks for the following requirements:
– Many parallel read and write accesses
– Frequent write accesses

Очевидно, при неправильном применении данный инструмент может стать узким горлышком и встать колом в неожиданный момент.

Так же в документации пишут, что никак нельзя создавать динамические типы, но мне не очень и хочется.

На сегодня всё, до новых встреч.