В обычной жизни мы не паримся с памятью в 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».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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
Сделаем простую реализацию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
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. |
Ничего сложного, есть айдишка сессии, берём-кладём значения, замечаем отметку время, когда сессия должна ликвидироваться.
Шаг второй
В реализацию метода интерфейса сразу добавим вызов конструктора, чтобы объект всегда существовал, хотя бы пустой для начала:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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, чтобы скрыть внутреннюю кухню:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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. |
И для того чтобы записать сессию почти что аналогично:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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
Очевидно, при неправильном применении данный инструмент может стать узким горлышком и встать колом в неожиданный момент.
Так же в документации пишут, что никак нельзя создавать динамические типы, но мне не очень и хочется.
На сегодня всё, до новых встреч.