mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 06:38:13 -05:00
Improved Sumo summary data fetching (#14551)
Sumo data is loaded so the application stays responsive while it waits. Run Sumo transfers on a dedicated thread Split RiaSumoConnector into transport and data delegates Load Sumo summary vectors concurrently and without waiting Show that Sumo data is being loaded
This commit is contained in:
1 parent
9b268e6d98
commit
0d9d4fed19
30 files changed
+2124
-920
No files matched your search
@@ -3,6 +3,8 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaCloudConnector.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaSumoConnector.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaSumoDefines.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaSumoExplore.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaSumoSummary.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaConnectorTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaOAuthHttpServerReplyHandler.cpp
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -20,50 +20,23 @@
|
||||
|
||||
#include "RiaCloudConnector.h"
|
||||
#include "RiaSumoDefines.h"
|
||||
#include "RiaSumoExplore.h"
|
||||
#include "RiaSumoSummary.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QtNetworkAuth/QOAuth2AuthorizationCodeFlow>
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <map>
|
||||
|
||||
class QEventLoop;
|
||||
class QThread;
|
||||
|
||||
using SumoObjectId = QString;
|
||||
|
||||
struct SumoAsset
|
||||
{
|
||||
SumoAssetId assetId;
|
||||
|
||||
QString kind;
|
||||
QString name;
|
||||
};
|
||||
|
||||
struct SumoCase
|
||||
{
|
||||
SumoCaseId caseId;
|
||||
|
||||
QString kind;
|
||||
QString name;
|
||||
};
|
||||
|
||||
struct SumoRedirect
|
||||
{
|
||||
SumoObjectId objectId;
|
||||
QString blobName;
|
||||
QString url;
|
||||
QString redirectBaseUri;
|
||||
QString redirectAuth;
|
||||
QByteArray contents;
|
||||
};
|
||||
|
||||
struct SumoEnsemble
|
||||
{
|
||||
SumoCaseId caseId;
|
||||
QString name;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
@@ -84,85 +57,87 @@ public:
|
||||
|
||||
QString server() const override;
|
||||
|
||||
void requestAssets();
|
||||
void requestAssetsBlocking();
|
||||
// Download blobs by id and return their contents. Getting a blob takes two round trips, one for the
|
||||
// pre-signed URI and one for the data, and a batch does each of those as one concurrent group.
|
||||
QByteArray downloadBlobBlocking( const QString& blobId );
|
||||
std::map<QString, QByteArray> downloadBlobsBlocking( const std::vector<QString>& blobIds );
|
||||
|
||||
void requestCasesForField( const QString& fieldName );
|
||||
void requestCasesForFieldBlocking( const QString& fieldName );
|
||||
// What Sumo holds: assets, cases, ensembles and realizations.
|
||||
RiaSumoExplore& explore();
|
||||
|
||||
void requestEnsembleByCasesId( const SumoCaseId& caseId );
|
||||
void requestEnsembleByCasesIdBlocking( const SumoCaseId& caseId );
|
||||
// The summary data of a case.
|
||||
RiaSumoSummary& summary();
|
||||
|
||||
void requestVectorNamesForEnsemble( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
void requestVectorNamesForEnsembleBlocking( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
// Transport used by the data specific delegates. Every request goes through the transfer thread, so
|
||||
// the calling thread waits without dispatching events.
|
||||
QByteArray getBlocking( const QString& url, const QString& progressText = {} );
|
||||
|
||||
void requestRealizationIdsForEnsemble( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
void requestRealizationIdsForEnsembleBlocking( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
// The REST API returns a blob id as a plain string, quoted by FastAPI.
|
||||
static QString blobIdFromBody( const QByteArray& body );
|
||||
|
||||
void requestParametersBlobIdForEnsemble( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
void requestParametersBlobIdForEnsembleBlocking( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
QByteArray requestParametersParquetDataBlocking( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
|
||||
void requestBlobIdForEnsemble( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName );
|
||||
void requestBlobIdForEnsembleBlocking( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName );
|
||||
|
||||
void requestBlobDownload( const QString& blobId );
|
||||
void requestBlobBySasUri( const QString& blobId, const QString& sasUri );
|
||||
|
||||
QByteArray requestParquetDataBlocking( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName );
|
||||
|
||||
std::vector<SumoAsset> assets() const;
|
||||
std::vector<SumoCase> cases() const;
|
||||
std::vector<QString> ensembleNamesForCase( const SumoCaseId& caseId ) const;
|
||||
std::vector<QString> vectorNames() const;
|
||||
std::vector<QString> realizationIds() const;
|
||||
std::vector<QString> blobIds() const;
|
||||
std::vector<SumoRedirect> blobContents() const;
|
||||
|
||||
public slots:
|
||||
void parseAssets( QNetworkReply* reply );
|
||||
void parseEnsembleNames( QNetworkReply* reply, const SumoCaseId& caseId );
|
||||
void parseCases( QNetworkReply* reply );
|
||||
void parseVectorNames( QNetworkReply* reply, const SumoCaseId& caseId, const QString& ensembleName );
|
||||
void parseRealizationNumbers( QNetworkReply* reply, const SumoCaseId& caseId, const QString& ensembleName );
|
||||
void parseBlobId( QNetworkReply* reply, const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName, bool isParameters );
|
||||
|
||||
void requestFailed( const QAbstractOAuth::Error error );
|
||||
void parquetDownloadComplete( const QString& blobId, const QByteArray&, const QString& url );
|
||||
|
||||
signals:
|
||||
void fileDownloadFinished( const QString& fileId, const QString& filePath );
|
||||
void casesFinished();
|
||||
void wellsFinished();
|
||||
void wellboresFinished( const QString& wellId );
|
||||
void wellboreTrajectoryFinished( const QString& wellboreId );
|
||||
void parquetDownloadFinished( const QByteArray& contents, const QString& url );
|
||||
void ensembleNamesFinished();
|
||||
void vectorNamesFinished();
|
||||
void blobIdFinished();
|
||||
void assetsFinished();
|
||||
void realizationIdsFinished();
|
||||
|
||||
private:
|
||||
void addStandardHeader( QNetworkRequest& networkRequest, const QString& token, const QString& contentType );
|
||||
|
||||
QNetworkReply* makeDownloadRequest( const QString& url, const QString& token, const QString& contentType );
|
||||
void requestParquetData( const QString& url, const QString& token );
|
||||
// Run work on the transfer thread and wait for it. Pass progressText to show the standard progress dialog
|
||||
// while waiting, worth doing for the transfers slow enough to be noticed and not for the small requests
|
||||
// that would only make it flash.
|
||||
void runOnTransferThreadBlocking( const std::function<void()>& work, const QString& progressText = {} );
|
||||
|
||||
// Run work on the transfer thread without waiting for it. The async data paths use this: the result is
|
||||
// delivered by a callback rather than by returning, so the calling thread carries on immediately.
|
||||
void runOnTransferThread( const std::function<void()>& work );
|
||||
|
||||
// Hand a call back to the thread the connector lives on, the one owning the user interface. Results of
|
||||
// async work are delivered through this, so a caller never has its data handed to it on another thread.
|
||||
void invokeOnConnectorThread( const std::function<void()>& work );
|
||||
|
||||
// Download one blob, calling onFinished with its contents. Call on the transfer thread, where onFinished
|
||||
// is called as well. Empty contents mean the transfer failed.
|
||||
void downloadBlobAsync( const QString& blobId, const std::function<void( const QByteArray& )>& onFinished );
|
||||
|
||||
// Abort a reply that has not finished in time, so an async chain reports a failure instead of hanging and
|
||||
// leaving whoever waits for the data waiting forever.
|
||||
static void abortIfNotFinishedWithin( QNetworkReply* reply, int timeoutMillis );
|
||||
|
||||
// The network manager belonging to the calling thread: the transfer thread manager when called from
|
||||
// there, otherwise the one owned by RiaCloudConnector on the GUI thread.
|
||||
QNetworkAccessManager* networkAccessManager();
|
||||
|
||||
// The token for a request issued from the transfer thread. token() reads objects owned by another thread.
|
||||
QString transferToken() const;
|
||||
|
||||
static void waitForRepliesToFinish( const std::vector<QNetworkReply*>& replies );
|
||||
|
||||
// Issue and collect the two round trips a blob transfer needs. Called on the transfer thread.
|
||||
std::map<QString, QByteArray> downloadBlobs( const std::vector<QString>& blobIds );
|
||||
|
||||
public slots:
|
||||
void requestFailed( const QAbstractOAuth::Error error );
|
||||
|
||||
private:
|
||||
// Call on the thread owning the authentication objects, before work is handed over.
|
||||
void cacheTransferToken();
|
||||
|
||||
static QString constructSasUri( const QString& blobStoreBaseUri, const QString& blobId, const QString& sasToken );
|
||||
|
||||
void wrapAndCallNetworkRequest( std::function<void()> requestCallable, const QMetaMethod& signalMethod );
|
||||
QString sasUriFromReply( QNetworkReply* reply, const QString& blobId );
|
||||
static QByteArray blobContentsFromReply( QNetworkReply* reply, const QString& sasUri );
|
||||
static QByteArray replyBody( QNetworkReply* reply, const QString& url );
|
||||
|
||||
private:
|
||||
std::function<QString()> m_serverUrlProvider;
|
||||
|
||||
std::vector<SumoAsset> m_assets;
|
||||
std::vector<SumoCase> m_cases;
|
||||
std::vector<QString> m_vectorNames;
|
||||
std::vector<QString> m_realizationIds;
|
||||
std::vector<SumoEnsemble> m_ensembleNames;
|
||||
RiaSumoExplore m_explore;
|
||||
RiaSumoSummary m_summary;
|
||||
|
||||
std::vector<QString> m_blobId;
|
||||
// Transfers run on their own thread so the calling thread can wait without dispatching events. Waiting on
|
||||
// a nested event loop on the GUI thread let the view update code re-enter a load that was still running,
|
||||
// and the same grid property was downloaded twice. Authentication stays on the GUI thread: the OAuth flow
|
||||
// opens a browser and its objects live there.
|
||||
QThread* m_transferThread = nullptr;
|
||||
QObject* m_transferContext = nullptr; // lives on the transfer thread
|
||||
QNetworkAccessManager* m_transferNetworkAccessManager = nullptr; // created on the transfer thread
|
||||
|
||||
std::vector<SumoRedirect> m_redirectInfo;
|
||||
// Written on the thread handing work over, read on the transfer thread running it.
|
||||
mutable QMutex m_transferTokenMutex;
|
||||
QString m_transferToken;
|
||||
};
|
||||
@@ -36,3 +36,11 @@ int RiaSumoDefines::requestTimeoutMillis()
|
||||
{
|
||||
return 10 * 1000;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
int RiaSumoDefines::asyncRequestTimeoutMillis()
|
||||
{
|
||||
return 5 * 60 * 1000;
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
using SumoAssetId = nonstd::ordered<QString, struct sumo_asset_tag>;
|
||||
using SumoCaseId = nonstd::ordered<QString, struct sumo_case_tag>;
|
||||
|
||||
@@ -29,4 +31,9 @@ namespace RiaSumoDefines
|
||||
{
|
||||
QString tokenPath();
|
||||
int requestTimeoutMillis();
|
||||
|
||||
// The timeout of a request nothing is waiting for. Only there so a request that never answers is eventually
|
||||
// given up on, and generous because a summary vector that has not been aggregated yet is produced on demand
|
||||
// by the request asking for it. Nothing is blocked while it runs, so waiting longer costs nothing.
|
||||
int asyncRequestTimeoutMillis();
|
||||
}; // namespace RiaSumoDefines
|
||||
@@ -0,0 +1,168 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2024- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RiaSumoExplore.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
#include "RiaQStringFormatter.h"
|
||||
#include "RiaSumoConnector.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUrl>
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RiaSumoExplore::RiaSumoExplore( RiaSumoConnector& connector )
|
||||
: m_connector( connector )
|
||||
{
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<SumoAsset> RiaSumoExplore::assets()
|
||||
{
|
||||
const QString url = QString( "%1/assets" ).arg( m_connector.server() );
|
||||
|
||||
return parseAssets( m_connector.getBlocking( url, "Loading assets from Sumo" ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<SumoCase> RiaSumoExplore::cases( const QString& assetName )
|
||||
{
|
||||
const QString url = QString( "%1/cases?asset_name=%2" ).arg( m_connector.server() ).arg( QString( QUrl::toPercentEncoding( assetName ) ) );
|
||||
|
||||
return parseCases( m_connector.getBlocking( url, QString( "Loading the cases of %1 from Sumo" ).arg( assetName ) ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QString> RiaSumoExplore::ensembleNames( const SumoCaseId& caseId )
|
||||
{
|
||||
const QString url = QString( "%1/cases/%2/ensembles" ).arg( m_connector.server() ).arg( caseId.get() );
|
||||
|
||||
return parseEnsembleNames( m_connector.getBlocking( url, "Loading ensembles from Sumo" ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QString> RiaSumoExplore::realizationIds( const SumoCaseId& caseId, const QString& ensembleName )
|
||||
{
|
||||
const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName );
|
||||
|
||||
const QString url =
|
||||
QString( "%1/cases/%2/ensembles/%3/realizations" ).arg( m_connector.server() ).arg( caseId.get() ).arg( encodedEnsembleName );
|
||||
|
||||
return parseRealizationIds( m_connector.getBlocking( url, "Loading realizations from Sumo" ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<SumoAsset> RiaSumoExplore::parseAssets( const QByteArray& body )
|
||||
{
|
||||
std::vector<SumoAsset> assets;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson( body );
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
// This json is an array of AssetInfo
|
||||
for ( const QJsonValue& assetInfo : jsonArray )
|
||||
{
|
||||
QString assetName = assetInfo["name"].toString();
|
||||
assets.push_back( SumoAsset{ SumoAssetId( "" ), "", assetName } );
|
||||
}
|
||||
|
||||
for ( const auto& asset : assets )
|
||||
{
|
||||
RiaLogging::debug( std::format( "Asset: {}", asset.name ) );
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<SumoCase> RiaSumoExplore::parseCases( const QByteArray& body )
|
||||
{
|
||||
std::vector<SumoCase> cases;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson( body );
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
for ( const QJsonValue& value : jsonArray )
|
||||
{
|
||||
QJsonObject caseObj = value.toObject();
|
||||
|
||||
QString id = caseObj["id"].toString();
|
||||
QString kind = "";
|
||||
QString name = caseObj["name"].toString();
|
||||
cases.push_back( SumoCase{ SumoCaseId( id ), kind, name } );
|
||||
}
|
||||
|
||||
RiaLogging::debug( std::format( "Case count : {}", cases.size() ) );
|
||||
|
||||
return cases;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QString> RiaSumoExplore::parseEnsembleNames( const QByteArray& body )
|
||||
{
|
||||
std::vector<QString> ensembleNames;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson( body );
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
for ( const QJsonValue& value : jsonArray )
|
||||
{
|
||||
QJsonObject ensembleObj = value.toObject();
|
||||
ensembleNames.push_back( ensembleObj["name"].toString() );
|
||||
}
|
||||
|
||||
RiaLogging::debug( std::format( "Ensemble count : {}", ensembleNames.size() ) );
|
||||
|
||||
return ensembleNames;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QString> RiaSumoExplore::parseRealizationIds( const QByteArray& body )
|
||||
{
|
||||
std::vector<QString> realizationIds;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson( body );
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
for ( const QJsonValue& value : jsonArray )
|
||||
{
|
||||
realizationIds.push_back( QString::number( value.toInt() ) );
|
||||
}
|
||||
|
||||
return realizationIds;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2024- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RiaSumoDefines.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class RiaSumoConnector;
|
||||
|
||||
struct SumoAsset
|
||||
{
|
||||
SumoAssetId assetId;
|
||||
|
||||
QString kind;
|
||||
QString name;
|
||||
};
|
||||
|
||||
struct SumoCase
|
||||
{
|
||||
SumoCaseId caseId;
|
||||
|
||||
QString kind;
|
||||
QString name;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
/// Finding your way around what Sumo holds: the assets available, the cases of an asset, the ensembles
|
||||
/// of a case and the realizations of an ensemble. Requests are made through RiaSumoConnector, which owns
|
||||
/// the connection and does the transfers, and every call returns its result rather than leaving it in
|
||||
/// shared state.
|
||||
//==================================================================================================
|
||||
class RiaSumoExplore
|
||||
{
|
||||
public:
|
||||
explicit RiaSumoExplore( RiaSumoConnector& connector );
|
||||
|
||||
std::vector<SumoAsset> assets();
|
||||
std::vector<SumoCase> cases( const QString& assetName );
|
||||
std::vector<QString> ensembleNames( const SumoCaseId& caseId );
|
||||
std::vector<QString> realizationIds( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
|
||||
private:
|
||||
static std::vector<SumoAsset> parseAssets( const QByteArray& body );
|
||||
static std::vector<SumoCase> parseCases( const QByteArray& body );
|
||||
static std::vector<QString> parseEnsembleNames( const QByteArray& body );
|
||||
static std::vector<QString> parseRealizationIds( const QByteArray& body );
|
||||
|
||||
private:
|
||||
RiaSumoConnector& m_connector;
|
||||
};
|
||||
@@ -0,0 +1,370 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2024- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RiaSumoSummary.h"
|
||||
|
||||
#include "RiaCloudDefines.h"
|
||||
#include "RiaLogging.h"
|
||||
#include "RiaSumoConnector.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RiaSumoSummary::RiaSumoSummary( RiaSumoConnector& connector )
|
||||
: m_connector( connector )
|
||||
{
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QString> RiaSumoSummary::vectorNames( const SumoCaseId& caseId, const QString& ensembleName )
|
||||
{
|
||||
const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName );
|
||||
|
||||
const QString url =
|
||||
QString( "%1/cases/%2/ensembles/%3/vector_list" ).arg( m_connector.server() ).arg( caseId.get() ).arg( encodedEnsembleName );
|
||||
|
||||
return parseVectorNames( m_connector.getBlocking( url ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QByteArray RiaSumoSummary::vectorData( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName )
|
||||
{
|
||||
const auto contentsByVectorName = vectorData( caseId, ensembleName, std::vector<QString>{ vectorName } );
|
||||
|
||||
if ( auto it = contentsByVectorName.find( vectorName ); it != contentsByVectorName.end() ) return it->second;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Fetch several summary vectors at the same time. The blob id requests are issued together and waited
|
||||
/// for as a group, and so are the transfers, turning 2N sequential round trips into 2 batched ones.
|
||||
///
|
||||
/// This matters more than the round trip count alone: a vector that has not been aggregated yet is
|
||||
/// produced on demand by the request that asks for it, so fetching serially costs the sum of those
|
||||
/// aggregations while fetching together costs roughly the slowest one.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::map<QString, QByteArray>
|
||||
RiaSumoSummary::vectorData( const SumoCaseId& caseId, const QString& ensembleName, const std::vector<QString>& vectorNames )
|
||||
{
|
||||
std::map<QString, QByteArray> contentsByVectorName;
|
||||
if ( vectorNames.empty() ) return contentsByVectorName;
|
||||
|
||||
// Drop duplicates, so a vector is never requested twice in one batch.
|
||||
std::vector<QString> namesToFetch;
|
||||
for ( const auto& vectorName : vectorNames )
|
||||
{
|
||||
if ( vectorName.isEmpty() ) continue;
|
||||
if ( std::ranges::find( namesToFetch, vectorName ) != namesToFetch.end() ) continue;
|
||||
|
||||
namesToFetch.push_back( vectorName );
|
||||
}
|
||||
|
||||
if ( namesToFetch.empty() ) return contentsByVectorName;
|
||||
|
||||
m_connector.runOnTransferThreadBlocking(
|
||||
[&]()
|
||||
{
|
||||
// Phase 1: resolve all blob ids concurrently.
|
||||
std::vector<QNetworkReply*> blobIdReplies;
|
||||
for ( const auto& vectorName : namesToFetch )
|
||||
{
|
||||
blobIdReplies.push_back( makeVectorBlobIdRequest( caseId, ensembleName, vectorName ) );
|
||||
}
|
||||
|
||||
RiaSumoConnector::waitForRepliesToFinish( blobIdReplies );
|
||||
|
||||
std::vector<QString> blobIds;
|
||||
for ( size_t i = 0; i < blobIdReplies.size(); i++ )
|
||||
{
|
||||
blobIds.push_back( blobIdFromReply( blobIdReplies[i], namesToFetch[i] ) );
|
||||
}
|
||||
|
||||
// Phase 2: download all resolved blobs as one group.
|
||||
std::vector<QString> blobIdsToDownload;
|
||||
for ( const auto& blobId : blobIds )
|
||||
{
|
||||
if ( !blobId.isEmpty() ) blobIdsToDownload.push_back( blobId );
|
||||
}
|
||||
|
||||
const auto contentsByBlobId = m_connector.downloadBlobs( blobIdsToDownload );
|
||||
|
||||
// Anything missing failed; the caller falls back to fetching it on its own later.
|
||||
for ( size_t i = 0; i < blobIds.size(); i++ )
|
||||
{
|
||||
if ( blobIds[i].isEmpty() ) continue;
|
||||
|
||||
if ( auto it = contentsByBlobId.find( blobIds[i] ); it != contentsByBlobId.end() )
|
||||
{
|
||||
contentsByVectorName[namesToFetch[i]] = it->second;
|
||||
}
|
||||
}
|
||||
},
|
||||
QString( "Loading %1 summary vector(s) from Sumo" ).arg( namesToFetch.size() ) );
|
||||
|
||||
return contentsByVectorName;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Fetch several summary vectors without waiting for any of them. Every vector is requested at once and
|
||||
/// onVectorReady is called for each one as it arrives, on the thread the connector lives on, so a caller can
|
||||
/// show each vector the moment it is there instead of when the slowest one is.
|
||||
///
|
||||
/// Empty contents mean that vector failed. The callback is called exactly once per requested vector, so a
|
||||
/// caller tracking what is still on its way can rely on all of them being accounted for.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaSumoSummary::vectorDataAsync( const SumoCaseId& caseId,
|
||||
const QString& ensembleName,
|
||||
const std::vector<QString>& vectorNames,
|
||||
const std::function<void( const QString&, const QByteArray& )>& onVectorReady )
|
||||
{
|
||||
if ( vectorNames.empty() || !onVectorReady ) return;
|
||||
|
||||
m_connector.runOnTransferThread(
|
||||
[this, caseId, ensembleName, vectorNames, onVectorReady]()
|
||||
{
|
||||
for ( const auto& vectorName : vectorNames )
|
||||
{
|
||||
auto deliver = [this, onVectorReady, vectorName]( const QByteArray& contents )
|
||||
{
|
||||
m_connector.invokeOnConnectorThread( [onVectorReady, vectorName, contents]() { onVectorReady( vectorName, contents ); } );
|
||||
};
|
||||
|
||||
auto blobIdReply = makeVectorBlobIdRequest( caseId, ensembleName, vectorName );
|
||||
if ( !blobIdReply )
|
||||
{
|
||||
deliver( {} );
|
||||
continue;
|
||||
}
|
||||
|
||||
// A vector that has not been aggregated yet is produced on demand by this request, which can
|
||||
// take a good while. Nothing is waiting on it, so it is given room to finish.
|
||||
RiaSumoConnector::abortIfNotFinishedWithin( blobIdReply, RiaSumoDefines::asyncRequestTimeoutMillis() );
|
||||
|
||||
QObject::connect( blobIdReply,
|
||||
&QNetworkReply::finished,
|
||||
blobIdReply,
|
||||
[this, blobIdReply, vectorName, deliver]()
|
||||
{
|
||||
const QString blobId = blobIdFromReply( blobIdReply, vectorName );
|
||||
if ( blobId.isEmpty() )
|
||||
{
|
||||
deliver( {} );
|
||||
return;
|
||||
}
|
||||
|
||||
m_connector.downloadBlobAsync( blobId, deliver );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QByteArray RiaSumoSummary::parameterData( const SumoCaseId& caseId, const QString& ensembleName )
|
||||
{
|
||||
const QString blobId = parameterBlobId( caseId, ensembleName );
|
||||
if ( blobId.isEmpty() ) return {};
|
||||
|
||||
return m_connector.downloadBlobBlocking( blobId );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaSumoSummary::vectorBlobId( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName )
|
||||
{
|
||||
const QString url = vectorBlobIdUrl( caseId, ensembleName, vectorName );
|
||||
|
||||
return logBlobId( RiaSumoConnector::blobIdFromBody( m_connector.getBlocking( url ) ), vectorName );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaSumoSummary::vectorBlobIdUrl( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName ) const
|
||||
{
|
||||
const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName );
|
||||
const QString encodedVectorName = QUrl::toPercentEncoding( vectorName );
|
||||
|
||||
return QString( "%1/cases/%2/ensembles/%3/vectors/%4/blob_id" )
|
||||
.arg( m_connector.server() )
|
||||
.arg( caseId.get() )
|
||||
.arg( encodedEnsembleName )
|
||||
.arg( encodedVectorName );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Issue the blob id request for one vector. The reply is returned unfinished, so the caller decides how
|
||||
/// to wait for it: one at a time, or several at once when batching.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QNetworkReply* RiaSumoSummary::makeVectorBlobIdRequest( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName )
|
||||
{
|
||||
QNetworkRequest networkRequest;
|
||||
networkRequest.setUrl( QUrl( vectorBlobIdUrl( caseId, ensembleName, vectorName ) ) );
|
||||
m_connector.addStandardHeader( networkRequest, m_connector.transferToken(), RiaCloudDefines::contentTypeJson() );
|
||||
|
||||
return m_connector.networkAccessManager()->get( networkRequest );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Read the blob id off a finished blob id reply. The reply is consumed and scheduled for deletion.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaSumoSummary::blobIdFromReply( QNetworkReply* reply, const QString& vectorName )
|
||||
{
|
||||
if ( !reply ) return {};
|
||||
|
||||
const bool failed = !reply->isFinished() || reply->error() != QNetworkReply::NoError;
|
||||
QByteArray body = failed ? QByteArray() : reply->readAll();
|
||||
|
||||
if ( failed )
|
||||
{
|
||||
RiaLogging::error(
|
||||
std::format( "Request blob ID failed for vector '{}': {}", vectorName.toStdString(), reply->errorString().toStdString() ) );
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
return logBlobId( RiaSumoConnector::blobIdFromBody( body ), vectorName );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaSumoSummary::logBlobId( const QString& blobId, const QString& vectorName )
|
||||
{
|
||||
if ( !blobId.isEmpty() )
|
||||
{
|
||||
RiaLogging::debug( std::format( "Received blob ID for vector '{}': {}", vectorName.toStdString(), blobId.toStdString() ) );
|
||||
}
|
||||
|
||||
return blobId;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaSumoSummary::parameterBlobIdUrl( const SumoCaseId& caseId, const QString& ensembleName ) const
|
||||
{
|
||||
const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName );
|
||||
|
||||
return QString( "%1/cases/%2/ensembles/%3/parameters/blob_id" ).arg( m_connector.server() ).arg( caseId.get() ).arg( encodedEnsembleName );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Issue the blob id request for the ensemble parameters. The reply is returned unfinished, so the caller
|
||||
/// decides how to wait for it.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QNetworkReply* RiaSumoSummary::makeParameterBlobIdRequest( const SumoCaseId& caseId, const QString& ensembleName )
|
||||
{
|
||||
QNetworkRequest networkRequest;
|
||||
networkRequest.setUrl( QUrl( parameterBlobIdUrl( caseId, ensembleName ) ) );
|
||||
m_connector.addStandardHeader( networkRequest, m_connector.transferToken(), RiaCloudDefines::contentTypeJson() );
|
||||
|
||||
return m_connector.networkAccessManager()->get( networkRequest );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Fetch the ensemble parameters without waiting, calling onParametersReady on the thread the connector
|
||||
/// lives on. Empty contents mean the request failed, and the callback is called exactly once.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaSumoSummary::parameterDataAsync( const SumoCaseId& caseId,
|
||||
const QString& ensembleName,
|
||||
const std::function<void( const QByteArray& )>& onParametersReady )
|
||||
{
|
||||
if ( !onParametersReady ) return;
|
||||
|
||||
m_connector.runOnTransferThread(
|
||||
[this, caseId, ensembleName, onParametersReady]()
|
||||
{
|
||||
auto deliver = [this, onParametersReady]( const QByteArray& contents )
|
||||
{ m_connector.invokeOnConnectorThread( [onParametersReady, contents]() { onParametersReady( contents ); } ); };
|
||||
|
||||
auto blobIdReply = makeParameterBlobIdRequest( caseId, ensembleName );
|
||||
if ( !blobIdReply )
|
||||
{
|
||||
deliver( {} );
|
||||
return;
|
||||
}
|
||||
|
||||
RiaSumoConnector::abortIfNotFinishedWithin( blobIdReply, RiaSumoDefines::asyncRequestTimeoutMillis() );
|
||||
|
||||
QObject::connect( blobIdReply,
|
||||
&QNetworkReply::finished,
|
||||
blobIdReply,
|
||||
[this, blobIdReply, deliver]()
|
||||
{
|
||||
const QString blobId = blobIdFromReply( blobIdReply, "parameters" );
|
||||
if ( blobId.isEmpty() )
|
||||
{
|
||||
deliver( {} );
|
||||
return;
|
||||
}
|
||||
|
||||
m_connector.downloadBlobAsync( blobId, deliver );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RiaSumoSummary::parameterBlobId( const SumoCaseId& caseId, const QString& ensembleName )
|
||||
{
|
||||
const QString blobId = RiaSumoConnector::blobIdFromBody( m_connector.getBlocking( parameterBlobIdUrl( caseId, ensembleName ) ) );
|
||||
|
||||
if ( !blobId.isEmpty() )
|
||||
{
|
||||
RiaLogging::debug( std::format( "Received blob ID for parameters: {}", blobId.toStdString() ) );
|
||||
}
|
||||
|
||||
return blobId;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QString> RiaSumoSummary::parseVectorNames( const QByteArray& body )
|
||||
{
|
||||
std::vector<QString> vectorNames;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson( body );
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
for ( const QJsonValue& value : jsonArray )
|
||||
{
|
||||
QJsonObject vectorObj = value.toObject();
|
||||
vectorNames.push_back( vectorObj["name"].toString() );
|
||||
}
|
||||
|
||||
return vectorNames;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2024- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RiaSumoDefines.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
class RiaSumoConnector;
|
||||
class QNetworkReply;
|
||||
|
||||
//==================================================================================================
|
||||
/// The summary data of a Sumo case: the vectors an ensemble has, their values, and the ensemble
|
||||
/// parameters. Requests are made through RiaSumoConnector, which owns the connection and does the
|
||||
/// transfers, and every call returns its result rather than leaving it in shared state.
|
||||
//==================================================================================================
|
||||
class RiaSumoSummary
|
||||
{
|
||||
public:
|
||||
explicit RiaSumoSummary( RiaSumoConnector& connector );
|
||||
|
||||
std::vector<QString> vectorNames( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
|
||||
// The values of one summary vector, for all realizations, as a parquet blob.
|
||||
QByteArray vectorData( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName );
|
||||
|
||||
// The same for several vectors at once, returned by vector name. The blob id requests are issued as one
|
||||
// concurrent group and so are the transfers, which matters because a vector that has not been aggregated
|
||||
// yet is produced on demand by the request asking for it.
|
||||
std::map<QString, QByteArray> vectorData( const SumoCaseId& caseId, const QString& ensembleName, const std::vector<QString>& vectorNames );
|
||||
|
||||
// The same again, but without waiting: all vectors are requested at once and onVectorReady is called for
|
||||
// each as it arrives, on the thread the connector lives on. Empty contents mean that vector failed, and
|
||||
// the callback is called exactly once per requested vector.
|
||||
void vectorDataAsync( const SumoCaseId& caseId,
|
||||
const QString& ensembleName,
|
||||
const std::vector<QString>& vectorNames,
|
||||
const std::function<void( const QString&, const QByteArray& )>& onVectorReady );
|
||||
|
||||
// The ensemble parameters, as a parquet blob.
|
||||
QByteArray parameterData( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
|
||||
// The same without waiting. Like the vectors, the parameters are aggregated on demand by the service, so
|
||||
// the first request for them can take a while and is not something to hold the user interface for.
|
||||
void parameterDataAsync( const SumoCaseId& caseId,
|
||||
const QString& ensembleName,
|
||||
const std::function<void( const QByteArray& )>& onParametersReady );
|
||||
|
||||
QString vectorBlobId( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName );
|
||||
QString parameterBlobId( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
|
||||
private:
|
||||
QString vectorBlobIdUrl( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName ) const;
|
||||
QString parameterBlobIdUrl( const SumoCaseId& caseId, const QString& ensembleName ) const;
|
||||
QNetworkReply* makeParameterBlobIdRequest( const SumoCaseId& caseId, const QString& ensembleName );
|
||||
QNetworkReply* makeVectorBlobIdRequest( const SumoCaseId& caseId, const QString& ensembleName, const QString& vectorName );
|
||||
static QString blobIdFromReply( QNetworkReply* reply, const QString& vectorName );
|
||||
static QString logBlobId( const QString& blobId, const QString& vectorName );
|
||||
|
||||
static std::vector<QString> parseVectorNames( const QByteArray& body );
|
||||
|
||||
private:
|
||||
RiaSumoConnector& m_connector;
|
||||
};
|
||||
@@ -240,6 +240,17 @@ void RiaLogging::appendLoggerInstance( std::unique_ptr<RiaLogger> loggerInstance
|
||||
sm_logger.push_back( std::move( loggerInstance ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RiaLogging::flushPendingMessages()
|
||||
{
|
||||
for ( const auto& logger : sm_logger )
|
||||
{
|
||||
logger->flushPendingMessages();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -52,6 +52,10 @@ public:
|
||||
virtual void warning( const char* message ) = 0;
|
||||
virtual void info( const char* message ) = 0;
|
||||
virtual void debug( const char* message ) = 0;
|
||||
|
||||
// Deliver messages a logger has accepted but not yet written out. A logger that has to hand messages from
|
||||
// a worker thread over to another thread can otherwise let them arrive after messages logged later.
|
||||
virtual void flushPendingMessages() {}
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
@@ -73,6 +77,10 @@ public:
|
||||
static void info( std::string_view message, std::string_view logKeyword = "" );
|
||||
static void debug( std::string_view message, std::string_view logKeyword = "" );
|
||||
|
||||
// Write out anything the loggers are holding, so messages logged from a worker thread appear before the
|
||||
// messages the waiting thread logs once the worker is done.
|
||||
static void flushPendingMessages();
|
||||
|
||||
static std::chrono::time_point<std::chrono::high_resolution_clock> currentTime();
|
||||
static void logElapsedTime( std::string_view message, const std::chrono::time_point<std::chrono::high_resolution_clock>& startTime );
|
||||
|
||||
|
||||
Reference in new issue
Block a user