Merge remote-tracking branch 'origin/dev-1.2' into development
Conflicts: pom.xml
30
README.rst
|
@ -84,6 +84,36 @@ Changes:
|
|||
- "Client Configuration" is now named "Connection Configuration"
|
||||
- "Export Exam Configuration" is now named "Export SEB Settings" and can be found in the "SEB Settings" view of an "Exam Configuration"
|
||||
|
||||
SEB Server Version 1.2.0 is out
|
||||
-------------------------------
|
||||
|
||||
New features:
|
||||
|
||||
- Integrated X.509 certificate store to upload and use X.509 certificate for new and upcoming features
|
||||
- Connection configuration encryption with X.509 certificate
|
||||
- Additional exam configuration attributes of later added features of the SEB config-tool
|
||||
- Default sorting and filtering for all lists
|
||||
- Deleting of SEB client logs on the SEB Client Logs view
|
||||
- Zoom meeting service integration for live proctoring (this is still an experimental feature)
|
||||
- Ability to switch live proctoring features like town-hall, one-to-one room or broadcasting, on and off
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Exam configuration import gives more and clear information about the purpose of different imports
|
||||
- Color picker is now initialized with selected color
|
||||
- Fixed user-account deactivation on user-account list
|
||||
- Fixed indicator list on exam shows only up to five entries
|
||||
- Fixed none scrolling action pane
|
||||
- Fixed exam import of Moodle LMS integration with different quizzes on same course
|
||||
- Various bug-fixes and improvements for distributed setup of SEB Server (separated and scaled webservice and guiservice)
|
||||
|
||||
Changes:
|
||||
|
||||
- Updated MariaDB version for integrated setups from version 10.3 to 10.5
|
||||
- Updated Spring Boot version from 2.1.0.RELEASE to 2.3.7.RELEASE
|
||||
- Build pipeline automatically build the SEB Server docker image and put it to docker hub now
|
||||
- New SEB Server docker setup (dockerhost) that pulls the needed images from docker-hub
|
||||
|
||||
|
||||
SEB - SEB Server Compatibility
|
||||
------------------------------
|
||||
|
|
66
docs/certificates.rst
Normal file
|
@ -0,0 +1,66 @@
|
|||
.. _certificates-label:
|
||||
|
||||
Certificates
|
||||
====================
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
Import and store X.509 certificates within SEB Server is a new feature since SEB Server version 1.2. This allows an institutional administrator
|
||||
or an exam administrator to upload and store certificates for further use within SEB Server.
|
||||
|
||||
.. note::
|
||||
Certificates uploaded to SEB Server are stored in a secured certificate-store that is stored within the data base rather then a
|
||||
usual file. The certificated store is password secured and only the binary data of the certificate store is places into the
|
||||
databease for securty reasons.
|
||||
|
||||
Once a certificate has been uploaded to SEB Server it can be used for various other features of SEB Server where a certificate is needed.
|
||||
One feature that also comes with the SEB Server version 1.2 is the ability to encrypt a connection configuration with a certificate
|
||||
that has the right purpose (Identity) to do so. If you need this feature already, please have a look at: :ref:`connection-configuration-label`
|
||||
Other planed features are the import of certificate encrypted exam configurations as well as to embed certificates within a exam configuration
|
||||
to allow certificate pinning between SEB and LMS.
|
||||
|
||||
There is currently certificate upload support for two version of certificates:
|
||||
|
||||
**PEM**
|
||||
|
||||
This are usually non password protected certificates in different file-formats. SEB Server currently supports the following PEM file-formats:
|
||||
|
||||
- .pem
|
||||
- .crt
|
||||
- .cer
|
||||
|
||||
**PKCS12**
|
||||
|
||||
This are usually password protected certificates in different file-formats. SEB Server currently supports the following PKCS12 file-formats:
|
||||
|
||||
- .p12
|
||||
- .pfx
|
||||
|
||||
.. image:: images/certificates/cert_list.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/certificates/cert_list.png
|
||||
|
||||
|
||||
Use Cases
|
||||
---------
|
||||
|
||||
**Upload a certificate**
|
||||
|
||||
- To upload and store a certificate of supported file type, please open the "SEB Configuration" section and select the "Certificates" page
|
||||
- You will see the list of known certificates from the SEB Server like in the picture above.
|
||||
- Choose "Import Certificate" from the right action pane and the upload dialog will open.
|
||||
- Within the upload dialog, select the certificate file on your local machine that you want to upload.
|
||||
- If the certificate is password protected, you will need to give the password to upload the certificate.
|
||||
- Chlick on "OK" to start the import.
|
||||
- If the import is successful the imported certificate will show up in the list. Otherwise SEB Server will display an error message with the reason of failure.
|
||||
|
||||
**Remove / delete a certificate**
|
||||
|
||||
- To permanently delete a stored certificate on SEB Server, please open the "SEB Configuration" section and select the "Certificates" page
|
||||
- You will see the list of known certificates from the SEB Server like in the picture above.
|
||||
- Please select the certificate you want to remove.
|
||||
- Choose "Remove Selected Certificates" from the right action pane and a configuration dialog will appear.
|
||||
- If you are sure to delete the selected certificate(s), click on "OK" to delete.
|
||||
- The deleted certificates disappear form the certificates list.
|
||||
|
|
@ -19,10 +19,15 @@ with context defines default values and also to be able to only see change the a
|
|||
This feature is currently in an experimental state and may be changed and / or expanded within future releases of SEB Server. See
|
||||
:ref:`config-template-label`
|
||||
|
||||
An new feature since SEB Server version 1.2 is the integrated certificate store where an administator is able to upload and register
|
||||
certificates. The certificates can then be used to encrypt and secure a connection configuration for example. Or as planed for another
|
||||
SEB Server release, to embed into an exam configuration for SEB to allow certificate pinning on SEB - LMS communication.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
connection_config
|
||||
exam_config
|
||||
config_template
|
||||
config_template
|
||||
certificates
|
|
@ -55,6 +55,7 @@ Short description of all attributes of a connection configuration:
|
|||
**Starting an Exam**; Will cause SEB to use this connection configuration settings on startup but won't change local SEB settings.
|
||||
**Configuring a connection**; Will cause SEB to use this connection configuration settings and also save it as local SEB settings.
|
||||
- **Configuration Password**: Used to encrypt the connection configuration with a password. A SEB client will prompt this password while loading a password protected connection configuration.
|
||||
- **Encrypt with Certificate**: Since version 1.2. Used to encrypt the connection configuration with a certificate. The same certificate must be known by a SEB client to be able to load the configuration.
|
||||
- **With Fallback**: Select this to see and define a fallback strategy for SEB clients using this connection configuration in case of SEB Server service unavailability.
|
||||
- **Fallback URL**: Defines a start URL that is loaded by the SEB client in a fallback case.
|
||||
- **Connection Attempts**: Defines the number of attempts a SEB client will try to unsuccessfully connect to the SEB Server service until it switches to the fallback case.
|
||||
|
@ -89,7 +90,7 @@ configuration settings in the following ways:
|
|||
|
||||
- Connection configuration with "Starting an Exam" setting and fallback strategy:
|
||||
Show warning with options "retry", "fallback" (load Fallback URL) and "quit".
|
||||
|
||||
|
||||
|
||||
Use Cases
|
||||
---------
|
||||
|
@ -135,3 +136,21 @@ that connects with this connection configuration will then receive an HTTP 401 U
|
|||
- Now use the "Deactivate Connection Configuration" action from the right action pane to deactivate the connection configuration.
|
||||
- The connection configuration is now deactivated and SEB client using this connection configuration are not able to connect to SEB Server anymore.
|
||||
|
||||
**Encrypt the Connection Configuration by password or certificate**
|
||||
|
||||
To secure the used connection configuration you want to encrypt it with either password or certificate encryption. If you encrypt a connection
|
||||
configuration by password, SEB will promt the user for the password while loading the configuration whereas by using certificate encryption,
|
||||
a SEB client must know the same certificate that is been used for encryption while loading the configuration.
|
||||
|
||||
- Sign in as an institutional administrator and select the "Connection Configuration" sub-menu of the "SEB Configuration" main-menu on the left.
|
||||
- Create an new connection configuration or use the list filter and / or the list navigation to find the needed connection configuration.
|
||||
- Fill in the settings as usual and for password encryption define a password in the "Configuration Password" field and confirm the password in the "Confirm Password" field.
|
||||
- For a certificate based encryption select one of the given certificates within the "Encrypt with Certificate" setting.
|
||||
- To upload new certificates that can be used for encryption, please refer to: :ref:`certificates-label`
|
||||
- "Use asymmetric only encryption" if you use SEB Clients with version before 2.2. For more details on this subject please see: `SEB Configuration <https://safeexambrowser.org/developer/seb-file-format.html>`_
|
||||
- Save and activate the connection configuration. The connection configuration will then be encrypted by either password or certificate on export.
|
||||
|
||||
.. image:: images/connection_config/encrypt.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/connection_config/encrypt.png
|
||||
|
||||
|
|
|
@ -3,18 +3,26 @@
|
|||
Live Proctoring
|
||||
===============
|
||||
|
||||
Live proctoring is a new and yet experimental feature of SEB and SEB Server. The main goal of the live proctoring feature is to support the integration
|
||||
of an external meeting service like Jitsi Meet or Zoom for example, within a SEB and SEB Server setup for an exam.
|
||||
In this scenario a SEB client is able to join a meeting and send audio and video data to the meeting where a proctor can watch the students during
|
||||
an exam within this meeting(s). The role of SEB Server in this scenario is mainly to bind a particular meeting service and automatically create
|
||||
meeting rooms, instruct connected SEB clients to join a certain meeting room. SEB Server can also instruct SEB clients to participate in the meeting by
|
||||
allowing connected SEB clients to receive audio and video as well as enabling chat functionality.
|
||||
|
||||
.. note::
|
||||
This feature is still in a prototype state and not all functionality meight work as expected. Basically the meeting features
|
||||
are given or restricted by the meeting service that is used and the API and GUI that is given by that meeting service client
|
||||
|
||||
To be able to use the integrated live proctoring feature with SEB and SEB Server you need an external meeting service that can be used by the SEB Server to automatically create meetings.
|
||||
If this feature is enabled, you are able to setup a live proctoring service for a specified exam that can be used in the monitoring later on while the exam is running and SEB clients are connected.
|
||||
|
||||
.. note::
|
||||
This feature is still in a prototype state and not all functionality meight work as expected.
|
||||
|
||||
To be able to use the live proctoring features within SEB Server you need a external meeting service that scales out for the need.
|
||||
Currently supported is `Jitsi Meet <https://jitsi.org/jitsi-meet/>`_ with JWT token authentication enabled.
|
||||
A `Zoom meeting service <https://zoom.us/>`_ integration is planed for a future release of SEB Server.
|
||||
To setup and configure a Jitsi Meet service for testing you can refer to the `Docker installation documentation <https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker>`_
|
||||
Currently supported meeting services are:
|
||||
- `Jitsi Meet <https://jitsi.org/jitsi-meet/>`_ with JWT token authentication enabled.
|
||||
- `Zoom meeting service <https://zoom.us/>`_ with an account plan that fits your need and supports the `Zoom API and Client SDKs <https://marketplace.zoom.us/docs/sdk/native-sdks/developer-accounts>`_.
|
||||
|
||||
To setup a live proctoring service for an exam, go to the view page of the exam and use the "Proctoring Settings" action on the right action pain to open up the proctoring settings dialog.
|
||||
|
||||
To bind and setup a live proctoring service in SEB Server for an exam, go to the view page of the exam and use the "Proctoring Settings" action on the right action pain to open up the proctoring settings dialog.
|
||||
|
||||
.. image:: images/exam/proctoringSettings.png
|
||||
:align: center
|
||||
|
@ -29,11 +37,52 @@ create these collecting rooms while SEB clients are connecting to the running ex
|
|||
|
||||
After you have all the settings set, use "OK" to confirm the settings. SEB Server will then try to connect to the meeting service with the given settings and check the access.
|
||||
|
||||
Another part of proctoring settings can be found in the "Exam Configuration" "SEB Settings". There is a new tab with the name "Proctoring" where all SEB settings for proctoring are available.
|
||||
These settings are directly used by a SEB client that supports the proctoring feature.
|
||||
The SEB client settings for proctoring can be found in the "Exam Configuration" "SEB Settings". There is a new tab with the name "Proctoring" where all SEB settings for proctoring are available.
|
||||
These settings are directly used by a SEB client that supports the proctoring feature. SEB will initialize with this settings when connecting to SEB Server and also will use this settings as default settings during an exam.
|
||||
|
||||
.. image:: images/exam/proctoringSEBSettings.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/proctoringSEBSettings.png
|
||||
|
||||
Since SEB Server version 1.2 there is also an Zoom service section that let you define the default SEB settings for a Zoom proctoring setup.
|
||||
Please be aware that not all of the settings are functioning yet. Please refer to the the settings tooltip to get a actual description of
|
||||
a particular SEB feature setting
|
||||
|
||||
**Jitsi Meet***
|
||||
|
||||
To setup and configure a Jitsi Meet service for testing you can refer to the `Docker installation documentation <https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker>`_
|
||||
|
||||
.. note::
|
||||
The Jitsi Meet integration works only with JWT authentication. Please refer to the above documentation for how to setup JWT authentication.
|
||||
|
||||
**Zoom**
|
||||
|
||||
To integrate Zoom meetings as a live proctoring service within SEB Server you need to have an appropriate Zoom account with API and SDK access.
|
||||
`Zoom API and Client SDKs <https://marketplace.zoom.us/docs/sdk/native-sdks/developer-accounts>`_.
|
||||
Then you need to setup the Zoom account to run with JWT apps as described:
|
||||
|
||||
- Login into your Zoom account and use "App Marketplace" to setup an API app account
|
||||
|
||||
.. image:: images/exam/zoom1.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/zoom1.png
|
||||
|
||||
- Within the Zoom Marketplace use "Develop" and select "Build App".
|
||||
|
||||
.. image:: images/exam/zoom2.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/zoom2.png
|
||||
|
||||
- Choose either JWT or SDK or both if needed for exams with different SEB clients
|
||||
|
||||
.. image:: images/exam/zoom3.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/zoom3.png
|
||||
|
||||
- Follow the instructions to create the API account and use the "App Key" and "App Secret" or SDK Key and SDK Secret within the setup in SEB Server
|
||||
|
||||
.. image:: images/exam/zoom4.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/exam/zoom4.png
|
||||
|
||||
|
||||
|
|
2
docs/files/webservice_seb-server.xml
Normal file
BIN
docs/images/certificates/cert_list.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
docs/images/connection_config/encrypt.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/images/exam/zoom1.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
docs/images/exam/zoom2.png
Normal file
After Width: | Height: | Size: 192 KiB |
BIN
docs/images/exam/zoom3.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
docs/images/exam/zoom4.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
docs/images/lmssetup/moodle_mobile.png
Normal file
After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 94 KiB |
|
@ -206,6 +206,20 @@ Once the client registration was successful the client id and client secret can
|
|||
|
||||
To be able to create an LMS Setup for Moodle you need a Moodle administrator or manager account. You can then use this account in the LMS Setup to connect the the LMS.
|
||||
|
||||
Since SEB Server uses some functions from the Moodles mobile API, you have to make sure the web services for mobile apps are enabled within your Moodle setup.
|
||||
To do so please login to Moodel with an administrator account and go to "Side Administration", scroll down to "Mobile App" and choose "Mobile Settings.
|
||||
|
||||
.. image:: images/lmssetup/moodle_mobile.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/documentation/docs/images/lmssetup/moodle_mobile.png
|
||||
|
||||
If you have a restrictive Moodle setup and troubles with the Moodle API account to use with SEB Server, please try to import the following
|
||||
Moodle role profile within youe Moodle instance. This profile will create a SEB Server role within Moodle that can be used to apply to an
|
||||
API account to be used with SEB Server. The role defines only the necessary privileges and functions needed for SEB Server communication.
|
||||
|
||||
Moodle role and account settings: :download:`XML <files/webservice_seb-server.xml>`
|
||||
|
||||
|
||||
|
||||
.. _lms-setup-rest-plugin-label:
|
||||
|
||||
|
|
|
@ -83,24 +83,29 @@ In the detail view you are also able to use the instructions "Quit SEB Client" a
|
|||
Live Proctoring
|
||||
---------------
|
||||
|
||||
.. note::
|
||||
This feature is still in a prototype state and not all functionality meight work as expected. Basically the meeting features
|
||||
are given or restricted by the meeting service that is used and the API and GUI that is given by that meeting service client
|
||||
|
||||
|
||||
**Collecting Rooms**
|
||||
|
||||
When the exam live proctoring feature is enabled for the running exam (see :ref:`sebProctoringSettings-label`), SEB Server will automatically create and collect
|
||||
connected SEB clients into so called collecting rooms. The size of this collecting rooms can be defined within the proctoring settings in the exam.
|
||||
One ore more proctor can then open such a collecting room and SEB Server will join the proctor to the meeting where the participants of this room can be seen.
|
||||
One proctor can then open such a collecting room by left-clicking on the room action.
|
||||
SEB Server will then open a proctoring window and join the proctor to the meeting where the participants of this room can be seen.
|
||||
|
||||
.. image:: images/monitoring/proctoringExam.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/proctoringExam.png
|
||||
|
||||
Within a live proctoring window you can user the enabled features of the used meeting service. And you are able to communicate with the SEB clients by using one
|
||||
or more of the following features:
|
||||
A proctor is also able to view the names of all participants of a room by right-clicking on the specified room action.
|
||||
SEB server will then open a dialog with a list of all participants. Double-clicking on a particular participant will automatically
|
||||
load the detailed monitoring view of that participant.
|
||||
|
||||
- **Start Audio Broadcast** Will enforce the SEB clients within the particular room to enable receive audio and a proctor can speak to the students that are in the meeting.
|
||||
- **Start Video Broadcast** Will enforce the SEB clients within the particular room to enable receive audio and video and proctor is shown to the students that are in the meeting and can speak to them as well.
|
||||
- **Enable Chat** Will enforce the SEB clients within the particular room to enable the chat feature and a proctor is able to chat with all students in the meeting.
|
||||
|
||||
.. image:: images/monitoring/proctoringWindow.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/proctoringWindow.png
|
||||
.. note::
|
||||
A collecting room, once created will live as long as the exam is running and not has been deleted. When the exam ends or is been deleted,
|
||||
the collecting room will automatically get deleted on the SEB Server's persistent storage as well as on the meeting service side if needed.
|
||||
|
||||
**Town-hall Feature**
|
||||
|
||||
|
@ -121,6 +126,22 @@ When the single room is closed the connected SEB clients is enforced to leave th
|
|||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/proctoringClient.png
|
||||
|
||||
**Boradcast Features**
|
||||
|
||||
Within a live proctoring window a proctor can use the enabled features of the integrated meeting service. And is able to communicate with the SEB clients by using one
|
||||
of the following features if available for the service:
|
||||
|
||||
- **Start Audio Broadcast** Will enforce the SEB clients within the particular room to enable receive audio and a proctor can speak to the students that are in the meeting.
|
||||
- **Start Video Broadcast** Will enforce the SEB clients within the particular room to enable receive audio and video and proctor is shown to the students that are in the meeting and can speak to them as well.
|
||||
- **Enable Chat** Will enforce the SEB clients within the particular room to enable the chat feature and a proctor is able to chat with all students in the meeting.
|
||||
|
||||
While a broadcast feature is enabled by a proctor, the SEB Server sends an instruction to each SEB client that is within the same meeting to display the meeting client.
|
||||
A Student as well as a proctor is then able to use all the features of the meeting client of the integrated meeting service.
|
||||
|
||||
.. image:: images/monitoring/proctoringWindow.png
|
||||
:align: center
|
||||
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/monitoring/proctoringWindow.png
|
||||
|
||||
|
||||
All SEB Client Logs
|
||||
-------------------
|
||||
|
@ -144,4 +165,20 @@ action form the right action pane to open up a pop-up containing all related inf
|
|||
|
||||
Currently there is no export functionality to export all interessting SEB client logs to a CSV table for example. But such a feature will probably come
|
||||
with a next version of SEB Server.
|
||||
|
||||
**Delete filtered client logs**
|
||||
|
||||
To delete all currently filtered client logs, please use the "Delete Logs" action form the right action pane.
|
||||
|
||||
.. note::
|
||||
On deletion, all available logs will permanantly be deleted from the persistent storage. So please make sure you want to delete all
|
||||
logs that are currently displayed in the list before deleting.
|
||||
|
||||
|
||||
**Known Issues**
|
||||
|
||||
- Within the Zoom service it often happens that a participant appear twice in a room or meeting. This is probably caused by SEB clients rejoining the meetings while rooms or feature settings are changed.
|
||||
- In Zoom it is not possible to fully control a participant microphone. Therefore it may happen that participant can hear each other even if no proctor is in the meeting.
|
||||
- Within Jitsi Meet service when a proctor leaves the room it currently happens that a random participant became host/moderator since it is not possible in Jitsi Meet to have a meeting without host. We try to mitigate the problem with the `moderator plugin <https://github.com/nvonahsen/jitsi-token-moderation-plugin>`_ or `Jitsi Meet SaS <https://jaas.8x8.vc/#/>`_
|
||||
- In both services while broadcasting, it is not guaranteed that a student always see the proctor. Usually the meeting service shows or pins the participant that is currently speaking automatically.
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.datalayer.batis;
|
||||
|
||||
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport.*;
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.apache.ibatis.annotations.Arg;
|
||||
|
@ -15,39 +18,85 @@ import org.apache.ibatis.annotations.ConstructorArgs;
|
|||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.ResultType;
|
||||
import org.apache.ibatis.annotations.SelectProvider;
|
||||
import org.apache.ibatis.annotations.UpdateProvider;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
|
||||
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
||||
import org.mybatis.dynamic.sql.select.SelectDSL;
|
||||
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
|
||||
import org.mybatis.dynamic.sql.update.UpdateDSL;
|
||||
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
|
||||
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
|
||||
|
||||
@Mapper
|
||||
public interface ClientEventLastPingMapper {
|
||||
|
||||
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
|
||||
Long num(SelectStatementProvider selectStatement);
|
||||
@ConstructorArgs({ @Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT) })
|
||||
Collection<Long> selectPingTimes(SelectStatementProvider selectStatement);
|
||||
|
||||
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
|
||||
@ConstructorArgs({ @Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT) })
|
||||
Long selectPingTime(SelectStatementProvider selectStatement);
|
||||
|
||||
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
|
||||
@ConstructorArgs({ @Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT, id = true) })
|
||||
Long selectPK(SelectStatementProvider selectStatement);
|
||||
|
||||
@UpdateProvider(type = SqlProviderAdapter.class, method = "update")
|
||||
int update(UpdateStatementProvider updateStatement);
|
||||
|
||||
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
|
||||
@ResultType(ClientEventLastPingRecord.class)
|
||||
@ConstructorArgs({
|
||||
@Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
|
||||
@Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT),
|
||||
@Arg(column = "server_time", javaType = Long.class, jdbcType = JdbcType.BIGINT)
|
||||
})
|
||||
Collection<ClientEventLastPingRecord> selectMany(SelectStatementProvider select);
|
||||
|
||||
default Long selectPingTimeByPrimaryKey(final Long id_) {
|
||||
return SelectDSL.selectWithMapper(
|
||||
this::selectPingTime,
|
||||
ClientEventRecordDynamicSqlSupport.serverTime.as("server_time"))
|
||||
.from(ClientEventRecordDynamicSqlSupport.clientEventRecord)
|
||||
.where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(id_))
|
||||
.build()
|
||||
.execute();
|
||||
}
|
||||
|
||||
default Long pingRecordIdByConnectionId(final Long connectionId) {
|
||||
return SelectDSL.selectDistinctWithMapper(
|
||||
this::selectPK,
|
||||
ClientEventRecordDynamicSqlSupport.id.as("id"))
|
||||
.from(ClientEventRecordDynamicSqlSupport.clientEventRecord)
|
||||
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
|
||||
.build()
|
||||
.execute();
|
||||
}
|
||||
|
||||
default QueryExpressionDSL<MyBatis3SelectModelAdapter<Collection<ClientEventLastPingRecord>>> selectByExample() {
|
||||
|
||||
return SelectDSL.selectWithMapper(
|
||||
this::selectMany,
|
||||
|
||||
ClientEventRecordDynamicSqlSupport.clientConnectionId.as("id"),
|
||||
ClientEventRecordDynamicSqlSupport.id.as("id"),
|
||||
ClientEventRecordDynamicSqlSupport.serverTime.as("server_time"))
|
||||
|
||||
.from(ClientEventRecordDynamicSqlSupport.clientEventRecord);
|
||||
}
|
||||
|
||||
default int updatePingTime(final Long _id, final Long pingTime) {
|
||||
return UpdateDSL.updateWithMapper(this::update, clientEventRecord)
|
||||
.set(serverTime).equalTo(pingTime)
|
||||
.where(id, isEqualTo(_id))
|
||||
.build()
|
||||
.execute();
|
||||
}
|
||||
|
||||
final class ClientEventLastPingRecord {
|
||||
|
||||
public final Long id;
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.util.Set;
|
|||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.joda.time.DateTimeUtils;
|
||||
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
@ -312,21 +313,6 @@ public class ClientEventDAOImpl implements ClientEventDAO {
|
|||
.stream()
|
||||
.map(pk -> new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// return all
|
||||
// .stream()
|
||||
// .map(EntityKey::getModelId)
|
||||
// .map(Long::parseLong)
|
||||
// .map(pk -> {
|
||||
// final int deleted = this.clientEventRecordMapper.deleteByPrimaryKey(pk);
|
||||
// if (deleted == 1) {
|
||||
// return new EntityKey(String.valueOf(pk), EntityType.CLIENT_EVENT);
|
||||
// } else {
|
||||
// return null;
|
||||
// }
|
||||
// })
|
||||
// .filter(Objects::nonNull)
|
||||
// .collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -345,7 +331,7 @@ public class ClientEventDAOImpl implements ClientEventDAO {
|
|||
return lastPingRec.get(0);
|
||||
}
|
||||
|
||||
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
|
||||
final ClientEventRecord clientEventRecord = new ClientEventRecord();
|
||||
clientEventRecord.setClientConnectionId(connectionId);
|
||||
clientEventRecord.setType(EventType.LAST_PING.id);
|
||||
|
|
|
@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
|
||||
|
||||
/** A Service to handle running exam sessions */
|
||||
|
@ -178,6 +179,15 @@ public interface ExamSessionService {
|
|||
* @return Result with reference to the given Exam or to an error if happened */
|
||||
Result<Exam> flushCache(final Exam exam);
|
||||
|
||||
/** Is is supposed to be the single access point to internally get client connection
|
||||
* data for a specified connection token.
|
||||
* This uses the client connection data cache for lookup and also synchronizes asynchronous
|
||||
* cache calls to prevent parallel creation of ClientConnectionDataInternal
|
||||
*
|
||||
* @param connectionToken the connection token of the active SEB client connection
|
||||
* @return ClientConnectionDataInternal by synchronized cache lookup or null if not available */
|
||||
ClientConnectionDataInternal getConnectionDataInternal(String connectionToken);
|
||||
|
||||
/** Checks if the given ClientConnectionData is an active SEB client connection.
|
||||
*
|
||||
* @param connection ClientConnectionData instance
|
||||
|
|
|
@ -308,13 +308,20 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnectionDataInternal getConnectionDataInternal(final String connectionToken) {
|
||||
synchronized (this.examSessionCacheService) {
|
||||
return this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ClientConnectionData> getConnectionData(final String connectionToken) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||
.getClientConnection(connectionToken);
|
||||
final ClientConnectionDataInternal activeClientConnection =
|
||||
getConnectionDataInternal(connectionToken);
|
||||
|
||||
if (activeClientConnection == null) {
|
||||
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
|
||||
|
@ -403,7 +410,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
.getConnectionTokens(examId)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(this.examSessionCacheService::getClientConnection)
|
||||
.map(this::getConnectionDataInternal)
|
||||
.filter(Objects::nonNull)
|
||||
.map(cc -> cc.getClientConnection().updateTime)
|
||||
.collect(Collectors.toSet());
|
||||
|
|
|
@ -45,6 +45,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.DistributedPingCache;
|
||||
import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException;
|
||||
|
||||
@Lazy
|
||||
|
@ -71,6 +72,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
private final SEBClientNotificationService sebClientNotificationService;
|
||||
private final WebserviceInfo webserviceInfo;
|
||||
private final ExamAdminService examAdminService;
|
||||
private final DistributedPingCache distributedPingCache;
|
||||
|
||||
protected SEBClientConnectionServiceImpl(
|
||||
final ExamSessionService examSessionService,
|
||||
|
@ -79,7 +81,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
final SEBClientConfigDAO sebClientConfigDAO,
|
||||
final SEBClientInstructionService sebInstructionService,
|
||||
final SEBClientNotificationService sebClientNotificationService,
|
||||
final ExamAdminService examAdminService) {
|
||||
final ExamAdminService examAdminService,
|
||||
final DistributedPingCache distributedPingCache) {
|
||||
|
||||
this.examSessionService = examSessionService;
|
||||
this.examSessionCacheService = examSessionService.getExamSessionCacheService();
|
||||
|
@ -91,6 +94,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
this.sebClientNotificationService = sebClientNotificationService;
|
||||
this.webserviceInfo = sebInstructionService.getWebserviceInfo();
|
||||
this.examAdminService = examAdminService;
|
||||
this.distributedPingCache = distributedPingCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -155,8 +159,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
.getOrThrow();
|
||||
|
||||
// load client connection data into cache
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||
.getClientConnection(connectionToken);
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionService
|
||||
.getConnectionDataInternal(connectionToken);
|
||||
|
||||
if (activeClientConnection == null) {
|
||||
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
|
||||
|
@ -453,6 +457,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
updatedClientConnection = clientConnection;
|
||||
}
|
||||
|
||||
// delete stored ping if this is a distributed setup
|
||||
if (this.webserviceInfo.isDistributed()) {
|
||||
this.distributedPingCache
|
||||
.deletePingForConnection(updatedClientConnection.id);
|
||||
}
|
||||
|
||||
reloadConnectionCache(connectionToken);
|
||||
return updatedClientConnection;
|
||||
});
|
||||
|
@ -501,6 +511,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
updatedClientConnection = clientConnection;
|
||||
}
|
||||
|
||||
// delete stored ping if this is a distributed setup
|
||||
if (this.webserviceInfo.isDistributed()) {
|
||||
this.distributedPingCache
|
||||
.deletePingForConnection(updatedClientConnection.id);
|
||||
}
|
||||
|
||||
reloadConnectionCache(connectionToken);
|
||||
return updatedClientConnection;
|
||||
});
|
||||
|
@ -510,6 +526,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
public void updatePingEvents() {
|
||||
try {
|
||||
|
||||
final boolean distributed = this.webserviceInfo.isDistributed();
|
||||
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
|
||||
final long now = Utils.getMillisecondsNow();
|
||||
final Consumer<ClientConnectionDataInternal> missingPingUpdate = missingPingUpdate(now);
|
||||
|
@ -518,7 +535,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
.allRunningExamIds()
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.flatMap(examId -> (this.webserviceInfo.isDistributed())
|
||||
.flatMap(examId -> distributed
|
||||
? this.clientConnectionDAO
|
||||
.getConnectionTokensNoCache(examId)
|
||||
.getOrThrow()
|
||||
|
@ -550,7 +567,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
final int pingNumber) {
|
||||
|
||||
final ClientConnectionDataInternal activeClientConnection =
|
||||
this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
this.examSessionService.getConnectionDataInternal(connectionToken);
|
||||
|
||||
if (activeClientConnection != null) {
|
||||
activeClientConnection.notifyPing(timestamp, pingNumber);
|
||||
|
@ -566,7 +583,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
|
||||
try {
|
||||
final ClientConnectionDataInternal activeClientConnection =
|
||||
this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
this.examSessionService.getConnectionDataInternal(connectionToken);
|
||||
|
||||
if (activeClientConnection != null) {
|
||||
|
||||
|
@ -731,7 +748,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
// evict cached ClientConnection
|
||||
this.examSessionCacheService.evictClientConnection(connectionToken);
|
||||
// and load updated ClientConnection into cache
|
||||
return this.examSessionCacheService.getClientConnection(connectionToken);
|
||||
return this.examSessionService.getConnectionDataInternal(connectionToken);
|
||||
}
|
||||
|
||||
private Consumer<ClientConnectionDataInternal> missingPingUpdate(final long now) {
|
||||
|
|
|
@ -23,6 +23,7 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
|
|||
protected Long connectionId;
|
||||
protected boolean cachingEnabled;
|
||||
protected boolean active = true;
|
||||
protected long persistentUpdateInterval = PERSISTENT_UPDATE_INTERVAL;
|
||||
protected long lastPersistentUpdate = 0;
|
||||
|
||||
protected boolean valueInitializes = false;
|
||||
|
@ -72,7 +73,7 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
|
|||
}
|
||||
|
||||
if (!this.cachingEnabled && this.active) {
|
||||
if (now - this.lastPersistentUpdate > PERSISTENT_UPDATE_INTERVAL) {
|
||||
if (now - this.lastPersistentUpdate > this.persistentUpdateInterval) {
|
||||
this.currentValue = computeValueAt(now);
|
||||
this.lastPersistentUpdate = now;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator {
|
|||
final boolean cachingEnabled) {
|
||||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
super.persistentUpdateInterval = 2 * Constants.SECOND_IN_MILLIS;
|
||||
|
||||
if (indicatorDefinition == null || StringUtils.isBlank(indicatorDefinition.tags)) {
|
||||
this.tags = null;
|
||||
|
|
|
@ -86,6 +86,7 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
|
|||
} else {
|
||||
return super.currentValue;
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get indicator number from persistent storage: {}", e.getMessage());
|
||||
return this.currentValue;
|
||||
|
|
|
@ -15,27 +15,31 @@ import java.util.Set;
|
|||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeUtils;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
|
||||
|
||||
public abstract class AbstractPingIndicator extends AbstractClientIndicator {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AbstractPingIndicator.class);
|
||||
|
||||
private static final long INTERVAL_FOR_PERSISTENT_UPDATE = Constants.SECOND_IN_MILLIS;
|
||||
|
||||
private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
|
||||
|
||||
protected final ClientEventDAO clientEventDAO;
|
||||
protected final DistributedPingCache distributedPingCache;
|
||||
|
||||
private long lastUpdate = 0;
|
||||
protected ClientEventRecord pingRecord = null;
|
||||
private final long lastUpdate = 0;
|
||||
protected Long pingRecord = null;
|
||||
|
||||
protected AbstractPingIndicator(final DistributedPingCache distributedPingCache) {
|
||||
|
||||
protected AbstractPingIndicator(final ClientEventDAO clientEventDAO) {
|
||||
super();
|
||||
this.clientEventDAO = clientEventDAO;
|
||||
this.distributedPingCache = distributedPingCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -47,10 +51,12 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
|
|||
|
||||
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
|
||||
|
||||
if (!this.cachingEnabled) {
|
||||
this.pingRecord = this.clientEventDAO
|
||||
.initPingEvent(this.connectionId)
|
||||
.getOr(null);
|
||||
if (!this.cachingEnabled && this.active) {
|
||||
try {
|
||||
this.pingRecord = this.distributedPingCache.initPingForConnection(this.connectionId);
|
||||
} catch (final Exception e) {
|
||||
this.pingRecord = this.distributedPingCache.getPingRecordIdForConnectionId(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,19 +65,39 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
|
|||
super.currentValue = now;
|
||||
super.lastPersistentUpdate = now;
|
||||
|
||||
if (!this.cachingEnabled && this.pingRecord != null) {
|
||||
if (!this.cachingEnabled) {
|
||||
|
||||
if (this.pingRecord == null) {
|
||||
tryRecoverPingRecord();
|
||||
if (this.pingRecord == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last ping time on persistent storage
|
||||
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
|
||||
if (millisecondsNow - this.lastUpdate > INTERVAL_FOR_PERSISTENT_UPDATE) {
|
||||
this.pingRecord.setClientTime(timestamp);
|
||||
this.pingRecord.setServerTime(millisecondsNow);
|
||||
this.clientEventDAO.updatePingEvent(this.pingRecord);
|
||||
this.lastUpdate = millisecondsNow;
|
||||
this.distributedPingCache.updatePing(this.pingRecord, millisecondsNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryRecoverPingRecord() {
|
||||
|
||||
if (log.isWarnEnabled()) {
|
||||
log.warn("*** Missing ping record for connection: {}. Try to recover...", this.connectionId);
|
||||
}
|
||||
|
||||
try {
|
||||
this.pingRecord = this.distributedPingCache.getPingRecordIdForConnectionId(this.connectionId);
|
||||
if (this.pingRecord == null) {
|
||||
this.pingRecord = this.distributedPingCache.initPingForConnection(this.connectionId);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to recover ping record for connection: {}", this.connectionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<EventType> observedEvents() {
|
||||
return this.EMPTY_SET;
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
|
||||
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isIn;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
|
||||
import org.joda.time.DateTimeUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@WebServiceProfile
|
||||
public class DistributedPingCache implements DisposableBean {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DistributedPingCache.class);
|
||||
|
||||
private final ClientEventLastPingMapper clientEventLastPingMapper;
|
||||
private final ClientEventRecordMapper clientEventRecordMapper;
|
||||
private ScheduledFuture<?> taskRef;
|
||||
|
||||
private final Map<Long, Long> pingCache = new ConcurrentHashMap<>();
|
||||
|
||||
public DistributedPingCache(
|
||||
final ClientEventLastPingMapper clientEventLastPingMapper,
|
||||
final ClientEventRecordMapper clientEventRecordMapper,
|
||||
final WebserviceInfo webserviceInfo,
|
||||
final TaskScheduler taskScheduler) {
|
||||
|
||||
this.clientEventLastPingMapper = clientEventLastPingMapper;
|
||||
this.clientEventRecordMapper = clientEventRecordMapper;
|
||||
if (webserviceInfo.isDistributed()) {
|
||||
try {
|
||||
this.taskRef = taskScheduler.scheduleAtFixedRate(this::updateCache, 1000);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to initialize distributed ping cache update task");
|
||||
this.taskRef = null;
|
||||
}
|
||||
} else {
|
||||
this.taskRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long initPingForConnection(final Long connectionId) {
|
||||
try {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.trace("*** Initialize ping record for SEB connection: {}", connectionId);
|
||||
}
|
||||
|
||||
final Long recordId = this.clientEventLastPingMapper
|
||||
.pingRecordIdByConnectionId(connectionId);
|
||||
|
||||
if (recordId == null) {
|
||||
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
|
||||
final ClientEventRecord clientEventRecord = new ClientEventRecord();
|
||||
clientEventRecord.setClientConnectionId(connectionId);
|
||||
clientEventRecord.setType(EventType.LAST_PING.id);
|
||||
clientEventRecord.setClientTime(millisecondsNow);
|
||||
clientEventRecord.setServerTime(millisecondsNow);
|
||||
this.clientEventRecordMapper.insert(clientEventRecord);
|
||||
|
||||
try {
|
||||
// This also double-check by trying again. If we have more then one entry here
|
||||
// this will throw an exception that causes a rollback
|
||||
return this.clientEventLastPingMapper
|
||||
.pingRecordIdByConnectionId(connectionId);
|
||||
|
||||
} catch (final Exception e) {
|
||||
|
||||
log.warn("Detected multiple client ping entries for connection: " + connectionId
|
||||
+ ". Force rollback to prevent");
|
||||
|
||||
// force rollback
|
||||
throw new RuntimeException("Detected multiple client ping entries");
|
||||
}
|
||||
}
|
||||
|
||||
return recordId;
|
||||
} catch (final Exception e) {
|
||||
|
||||
log.error("Failed to initialize ping for connection -> {}", connectionId, e);
|
||||
|
||||
// force rollback
|
||||
throw new RuntimeException("Failed to initialize ping for connection -> " + connectionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Long getPingRecordIdForConnectionId(final Long connectionId) {
|
||||
try {
|
||||
|
||||
return this.clientEventLastPingMapper
|
||||
.pingRecordIdByConnectionId(connectionId);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get ping record for connection id: {} cause: {}", connectionId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updatePing(final Long pingRecordId, final Long pingTime) {
|
||||
try {
|
||||
|
||||
this.clientEventLastPingMapper
|
||||
.updatePingTime(pingRecordId, pingTime);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to update ping for ping record id -> {}", pingRecordId);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deletePingForConnection(final Long connectionId) {
|
||||
try {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("*** Delete ping record for SEB connection: {}", connectionId);
|
||||
}
|
||||
|
||||
this.clientEventRecordMapper
|
||||
.deleteByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to delete ping for connection -> {}", connectionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getLastPing(final Long pingRecordId) {
|
||||
try {
|
||||
Long ping = this.pingCache.get(pingRecordId);
|
||||
if (ping == null) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("*** Get and cache ping time: {}", pingRecordId);
|
||||
}
|
||||
|
||||
ping = this.clientEventLastPingMapper.selectPingTimeByPrimaryKey(pingRecordId);
|
||||
if (ping != null) {
|
||||
this.pingCache.put(pingRecordId, ping);
|
||||
}
|
||||
}
|
||||
|
||||
return ping;
|
||||
} catch (final Exception e) {
|
||||
log.error("Error while trying to get last ping from storage: {}", e.getMessage());
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateCache() {
|
||||
|
||||
if (this.pingCache.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.trace("*** Update distributed ping cache: {}", this.pingCache);
|
||||
}
|
||||
|
||||
try {
|
||||
final ArrayList<Long> pks = new ArrayList<>(this.pingCache.keySet());
|
||||
final Map<Long, Long> mapping = this.clientEventLastPingMapper
|
||||
.selectByExample()
|
||||
.where(
|
||||
ClientEventRecordDynamicSqlSupport.id,
|
||||
isIn(pks))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(entry -> entry.id, entry -> entry.lastPingTime));
|
||||
|
||||
if (mapping != null) {
|
||||
this.pingCache.clear();
|
||||
this.pingCache.putAll(mapping);
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Error while trying to update distributed ping cache: {}", this.pingCache, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (this.taskRef != null) {
|
||||
try {
|
||||
final boolean cancel = this.taskRef.cancel(true);
|
||||
if (!cancel) {
|
||||
log.warn("Failed to cancel distributed ping cache update task");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to cancel distributed ping cache update task: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -26,9 +26,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
|
||||
|
||||
@Lazy
|
||||
@Component(IndicatorType.Names.LAST_PING)
|
||||
|
@ -46,8 +44,8 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
|
|||
private boolean missingPing = false;
|
||||
private boolean hidden = false;
|
||||
|
||||
public PingIntervalClientIndicator(final ClientEventDAO clientEventDAO) {
|
||||
super(clientEventDAO);
|
||||
public PingIntervalClientIndicator(final DistributedPingCache distributedPingCache) {
|
||||
super(distributedPingCache);
|
||||
this.cachingEnabled = true;
|
||||
}
|
||||
|
||||
|
@ -129,17 +127,10 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
|
|||
|
||||
// if this indicator is not missing ping
|
||||
if (!this.isMissingPing()) {
|
||||
|
||||
final Result<Long> lastPing = this.clientEventDAO
|
||||
.getLastPing(super.pingRecord.getId());
|
||||
|
||||
if (!lastPing.hasError()) {
|
||||
if (Double.isNaN(this.currentValue)) {
|
||||
return lastPing.get().doubleValue();
|
||||
}
|
||||
return Math.max(this.currentValue, lastPing.get().doubleValue());
|
||||
} else {
|
||||
log.error("Failed to get last ping from persistent: {}", lastPing.getError().getMessage());
|
||||
final Long lastPing = this.distributedPingCache.getLastPing(super.pingRecord);
|
||||
if (lastPing != null) {
|
||||
final double doubleValue = lastPing.doubleValue();
|
||||
return Math.max(Double.isNaN(this.currentValue) ? doubleValue : this.currentValue, doubleValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -270,7 +270,7 @@ public class ExamAPI_V1_Controller {
|
|||
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("****************** SEB client connection: {} ip: ",
|
||||
log.trace("****************** SEB client connection: {} ip: {}",
|
||||
connectionToken,
|
||||
getClientAddress(request));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
server.address=localhost
|
||||
server.port=8080
|
||||
server.port=8090
|
||||
|
||||
sebserver.gui.http.external.scheme=http
|
||||
sebserver.gui.entrypoint=/gui
|
||||
sebserver.gui.webservice.protocol=http
|
||||
sebserver.gui.webservice.address=localhost
|
||||
sebserver.gui.webservice.port=8080
|
||||
sebserver.gui.webservice.port=8090
|
||||
sebserver.gui.webservice.apipath=/admin-api/v1
|
||||
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
|
||||
sebserver.gui.webservice.poll-interval=1000
|
||||
|
|
|
@ -22,7 +22,7 @@ sebserver.webservice.clean-db-on-startup=false
|
|||
|
||||
# webservice configuration
|
||||
sebserver.init.adminaccount.gen-on-init=false
|
||||
sebserver.webservice.distributed=false
|
||||
sebserver.webservice.distributed=true
|
||||
sebserver.webservice.master.delay.threshold=10000
|
||||
sebserver.webservice.http.external.scheme=http
|
||||
sebserver.webservice.http.external.servername=localhost
|
||||
|
|
|
@ -33,9 +33,10 @@ public class PingIntervalClientIndicatorTest {
|
|||
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
|
||||
|
||||
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
|
||||
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
|
||||
|
||||
final PingIntervalClientIndicator pingIntervalClientIndicator =
|
||||
new PingIntervalClientIndicator(clientEventDAO);
|
||||
new PingIntervalClientIndicator(distributedPingCache);
|
||||
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
|
||||
}
|
||||
|
||||
|
@ -45,9 +46,10 @@ public class PingIntervalClientIndicatorTest {
|
|||
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
|
||||
|
||||
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
|
||||
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
|
||||
|
||||
final PingIntervalClientIndicator pingIntervalClientIndicator =
|
||||
new PingIntervalClientIndicator(clientEventDAO);
|
||||
new PingIntervalClientIndicator(distributedPingCache);
|
||||
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
|
||||
|
||||
DateTimeUtils.setCurrentMillisProvider(() -> 10L);
|
||||
|
@ -60,9 +62,10 @@ public class PingIntervalClientIndicatorTest {
|
|||
DateTimeUtils.setCurrentMillisProvider(() -> 1L);
|
||||
|
||||
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
|
||||
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
|
||||
|
||||
final PingIntervalClientIndicator pingIntervalClientIndicator =
|
||||
new PingIntervalClientIndicator(clientEventDAO);
|
||||
new PingIntervalClientIndicator(distributedPingCache);
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator);
|
||||
assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);
|
||||
|
|