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:
Jørgen Herje authored and GitHub committed 2026-08-20 11:48:25 +02:00
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 );