В обычной жизни мы не паримся с памятью в 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
Очевидно, при неправильном применении данный инструмент может стать узким горлышком и встать колом в неожиданный момент.
Так же в документации пишут, что никак нельзя создавать динамические типы, но мне не очень и хочется.
На сегодня всё, до новых встреч.