diff --git a/apps/ogr2ogr_lib.cpp b/apps/ogr2ogr_lib.cpp index 377d27ec4baa..28866b20c7d5 100644 --- a/apps/ogr2ogr_lib.cpp +++ b/apps/ogr2ogr_lib.cpp @@ -6052,46 +6052,6 @@ bool LayerTranslator::TranslateArrow( schema.release(&schema); - // Ugly hack to work around https://github.com/OSGeo/gdal/issues/9497 - // Deleting a RecordBatchReader obtained from arrow::dataset::Scanner.ToRecordBatchReader() - // is a lengthy operation since all batches are read in its destructors. - // Here we ask to our custom I/O layer to return in error to short circuit - // that lengthy operation. - if (auto poDS = psInfo->m_poSrcLayer->GetDataset()) - { - if (poDS->GetLayerCount() == 1 && poDS->GetDriver() && - EQUAL(poDS->GetDriver()->GetDescription(), "PARQUET")) - { - bool bStopIO = false; - const char *pszArrowStopIO = - CPLGetConfigOption("OGR_ARROW_STOP_IO", nullptr); - if (pszArrowStopIO && CPLTestBool(pszArrowStopIO)) - { - bStopIO = true; - } - else if (!pszArrowStopIO) - { - std::string osExePath; - osExePath.resize(1024); - if (CPLGetExecPath(osExePath.data(), - static_cast(osExePath.size()))) - { - osExePath.resize(strlen(osExePath.data())); - if (strcmp(CPLGetBasename(osExePath.data()), "ogr2ogr") == - 0) - { - bStopIO = true; - } - } - } - if (bStopIO) - { - CPLSetConfigOption("OGR_ARROW_STOP_IO", "YES"); - CPLDebug("OGR2OGR", "Forcing interruption of Parquet I/O"); - } - } - } - stream.release(&stream); return bRet; } diff --git a/ogr/ogrsf_frmts/arrow/ogrfeatherdriver.cpp b/ogr/ogrsf_frmts/arrow/ogrfeatherdriver.cpp index 27639faf4421..4fe00174df31 100644 --- a/ogr/ogrsf_frmts/arrow/ogrfeatherdriver.cpp +++ b/ogr/ogrsf_frmts/arrow/ogrfeatherdriver.cpp @@ -94,8 +94,8 @@ static bool IsArrowIPCStream(GDALOpenInfo *poOpenInfo) auto fp = VSIVirtualHandleUniquePtr(VSIFileFromMemBuffer( osTmpFilename.c_str(), poOpenInfo->pabyHeader, nSizeToRead, false)); - auto infile = - std::make_shared(std::move(fp)); + auto infile = std::make_shared( + osTmpFilename.c_str(), std::move(fp)); auto options = arrow::ipc::IpcReadOptions::Defaults(); auto result = arrow::ipc::RecordBatchStreamReader::Open(infile, options); @@ -113,8 +113,8 @@ static bool IsArrowIPCStream(GDALOpenInfo *poOpenInfo) return false; // Do not give ownership of poOpenInfo->fpL to infile - auto infile = - std::make_shared(poOpenInfo->fpL, false); + auto infile = std::make_shared( + poOpenInfo->pszFilename, poOpenInfo->fpL, false); auto options = arrow::ipc::IpcReadOptions::Defaults(); auto result = arrow::ipc::RecordBatchStreamReader::Open(infile, options); @@ -164,14 +164,16 @@ static GDALDataset *OGRFeatherDriverOpen(GDALOpenInfo *poOpenInfo) osFilename.c_str()); return nullptr; } - infile = std::make_shared(std::move(fp)); + infile = std::make_shared(osFilename.c_str(), + std::move(fp)); } else if (STARTS_WITH(poOpenInfo->pszFilename, "/vsi") || CPLTestBool(CPLGetConfigOption("OGR_ARROW_USE_VSI", "NO"))) { VSIVirtualHandleUniquePtr fp(poOpenInfo->fpL); poOpenInfo->fpL = nullptr; - infile = std::make_shared(std::move(fp)); + infile = std::make_shared( + poOpenInfo->pszFilename, std::move(fp)); } else { diff --git a/ogr/ogrsf_frmts/arrow_common/ogr_arrow.h b/ogr/ogrsf_frmts/arrow_common/ogr_arrow.h index 6179fd009b07..3f09195c1987 100644 --- a/ogr/ogrsf_frmts/arrow_common/ogr_arrow.h +++ b/ogr/ogrsf_frmts/arrow_common/ogr_arrow.h @@ -352,6 +352,13 @@ class OGRArrowDataset CPL_NON_FINAL : public GDALPamDataset std::vector m_aosDomainNames{}; std::map m_oMapDomainNameToCol{}; + protected: + void close() + { + m_poLayer.reset(); + m_poMemoryPool.reset(); + } + public: explicit OGRArrowDataset( const std::shared_ptr &poMemoryPool); diff --git a/ogr/ogrsf_frmts/arrow_common/ograrrowrandomaccessfile.h b/ogr/ogrsf_frmts/arrow_common/ograrrowrandomaccessfile.h index cd16ddd14868..a1c4a463459a 100644 --- a/ogr/ogrsf_frmts/arrow_common/ograrrowrandomaccessfile.h +++ b/ogr/ogrsf_frmts/arrow_common/ograrrowrandomaccessfile.h @@ -36,6 +36,9 @@ #include "arrow/io/file.h" #include "arrow/io/interfaces.h" +#include +#include + /************************************************************************/ /* OGRArrowRandomAccessFile */ /************************************************************************/ @@ -43,22 +46,58 @@ class OGRArrowRandomAccessFile final : public arrow::io::RandomAccessFile { int64_t m_nSize = -1; + const std::string m_osFilename; VSILFILE *m_fp; - bool m_bOwnFP; + const bool m_bOwnFP; + std::atomic m_bAskedToClosed = false; + +#ifdef OGR_ARROW_USE_PREAD + const bool m_bDebugReadAt; + const bool m_bUsePRead; +#endif OGRArrowRandomAccessFile(const OGRArrowRandomAccessFile &) = delete; OGRArrowRandomAccessFile & operator=(const OGRArrowRandomAccessFile &) = delete; public: - explicit OGRArrowRandomAccessFile(VSILFILE *fp, bool bOwnFP) - : m_fp(fp), m_bOwnFP(bOwnFP) + OGRArrowRandomAccessFile(const std::string &osFilename, VSILFILE *fp, + bool bOwnFP) + : m_osFilename(osFilename), m_fp(fp), m_bOwnFP(bOwnFP) +#ifdef OGR_ARROW_USE_PREAD + , + m_bDebugReadAt(!VSIIsLocal(m_osFilename.c_str())), + // Due to the lack of caching for current /vsicurl PRead(), do not + // use the PRead() implementation on those files + m_bUsePRead(m_fp->HasPRead() && + CPLTestBool(CPLGetConfigOption( + "OGR_ARROW_USE_PREAD", + VSIIsLocal(m_osFilename.c_str()) ? "YES" : "NO"))) +#endif + { + } + + OGRArrowRandomAccessFile(const std::string &osFilename, + VSIVirtualHandleUniquePtr &&fp) + : m_osFilename(osFilename), m_fp(fp.release()), m_bOwnFP(true) +#ifdef OGR_ARROW_USE_PREAD + , + m_bDebugReadAt(!VSIIsLocal(m_osFilename.c_str())), + // Due to the lack of caching for current /vsicurl PRead(), do not + // use the PRead() implementation on those files + m_bUsePRead(m_fp->HasPRead() && + CPLTestBool(CPLGetConfigOption( + "OGR_ARROW_USE_PREAD", + VSIIsLocal(m_osFilename.c_str()) ? "YES" : "NO"))) +#endif { } - explicit OGRArrowRandomAccessFile(VSIVirtualHandleUniquePtr &&fp) - : m_fp(fp.release()), m_bOwnFP(true) + void AskToClose() { + m_bAskedToClosed = true; + if (m_fp) + m_fp->Interrupt(); } ~OGRArrowRandomAccessFile() override @@ -85,11 +124,14 @@ class OGRArrowRandomAccessFile final : public arrow::io::RandomAccessFile bool closed() const override { - return m_fp == nullptr; + return m_bAskedToClosed || m_fp == nullptr; } arrow::Status Seek(int64_t position) override { + if (m_bAskedToClosed) + return arrow::Status::IOError("File requested to close"); + if (VSIFSeekL(m_fp, static_cast(position), SEEK_SET) == 0) return arrow::Status::OK(); return arrow::Status::IOError("Error while seeking"); @@ -97,6 +139,9 @@ class OGRArrowRandomAccessFile final : public arrow::io::RandomAccessFile arrow::Result Read(int64_t nbytes, void *out) override { + if (m_bAskedToClosed) + return arrow::Status::IOError("File requested to close"); + CPLAssert(static_cast(static_cast(nbytes)) == nbytes); return static_cast( VSIFReadL(out, 1, static_cast(nbytes), m_fp)); @@ -104,12 +149,10 @@ class OGRArrowRandomAccessFile final : public arrow::io::RandomAccessFile arrow::Result> Read(int64_t nbytes) override { + if (m_bAskedToClosed) + return arrow::Status::IOError("File requested to close"); + // CPLDebug("ARROW", "Reading %d bytes", int(nbytes)); - // Ugly hack for https://github.com/OSGeo/gdal/issues/9497 - if (CPLGetConfigOption("OGR_ARROW_STOP_IO", nullptr)) - { - return arrow::Result>(); - } auto buffer = arrow::AllocateResizableBuffer(nbytes); if (!buffer.ok()) { @@ -122,8 +165,54 @@ class OGRArrowRandomAccessFile final : public arrow::io::RandomAccessFile return buffer; } +#ifdef OGR_ARROW_USE_PREAD + using arrow::io::RandomAccessFile::ReadAt; + + arrow::Result> + ReadAt(int64_t position, int64_t nbytes) override + { + if (m_bAskedToClosed) + return arrow::Status::IOError("File requested to close"); + + if (m_bUsePRead) + { + auto buffer = arrow::AllocateResizableBuffer(nbytes); + if (!buffer.ok()) + { + return buffer; + } + if (m_bDebugReadAt) + { + CPLDebug( + "ARROW", + "Start ReadAt() called on %s (this=%p) from " + "thread=" CPL_FRMT_GIB ": pos=%" PRId64 ", nbytes=%" PRId64, + m_osFilename.c_str(), this, CPLGetPID(), position, nbytes); + } + uint8_t *buffer_data = (*buffer)->mutable_data(); + auto nread = m_fp->PRead(buffer_data, static_cast(nbytes), + static_cast(position)); + CPL_IGNORE_RET_VAL( + (*buffer)->Resize(nread)); // shrink --> cannot fail + if (m_bDebugReadAt) + { + CPLDebug( + "ARROW", + "End ReadAt() called on %s (this=%p) from " + "thread=" CPL_FRMT_GIB ": pos=%" PRId64 ", nbytes=%" PRId64, + m_osFilename.c_str(), this, CPLGetPID(), position, nbytes); + } + return buffer; + } + return arrow::io::RandomAccessFile::ReadAt(position, nbytes); + } +#endif + arrow::Result GetSize() override { + if (m_bAskedToClosed) + return arrow::Status::IOError("File requested to close"); + if (m_nSize < 0) { const auto nPos = VSIFTellL(m_fp); diff --git a/ogr/ogrsf_frmts/arrow_common/vsiarrowfilesystem.hpp b/ogr/ogrsf_frmts/arrow_common/vsiarrowfilesystem.hpp index 6a259e5d880b..0f50d4162ebe 100644 --- a/ogr/ogrsf_frmts/arrow_common/vsiarrowfilesystem.hpp +++ b/ogr/ogrsf_frmts/arrow_common/vsiarrowfilesystem.hpp @@ -33,6 +33,12 @@ #include "ograrrowrandomaccessfile.h" +#include +#include +#include +#include +#include + /************************************************************************/ /* VSIArrowFileSystem */ /************************************************************************/ @@ -42,14 +48,54 @@ class VSIArrowFileSystem final : public arrow::fs::FileSystem const std::string m_osEnvVarPrefix; const std::string m_osQueryParameters; + std::atomic m_bAskedToClosed = false; + std::mutex m_oMutex{}; + std::vector>> + m_oSetFiles{}; + public: - explicit VSIArrowFileSystem(const std::string &osEnvVarPrefix, - const std::string &osQueryParameters) + VSIArrowFileSystem(const std::string &osEnvVarPrefix, + const std::string &osQueryParameters) : m_osEnvVarPrefix(osEnvVarPrefix), m_osQueryParameters(osQueryParameters) { } + // Cf comment in OGRParquetDataset::~OGRParquetDataset() for rationale + // for this method + void AskToClose() + { + m_bAskedToClosed = true; + std::vector< + std::pair>> + oSetFiles; + { + std::lock_guard oLock(m_oMutex); + oSetFiles = m_oSetFiles; + } + for (auto &[osName, poFile] : oSetFiles) + { + bool bWarned = false; + while (!poFile.expired()) + { + if (!bWarned) + { + bWarned = true; + auto poFileLocked = poFile.lock(); + if (poFileLocked) + { + CPLDebug("PARQUET", + "Still on-going reads on %s. Waiting for it " + "to be closed.", + osName.c_str()); + poFileLocked->AskToClose(); + } + } + CPLSleep(0.01); + } + } + } + std::string type_name() const override { return "vsi" + m_osEnvVarPrefix; @@ -203,6 +249,10 @@ class VSIArrowFileSystem final : public arrow::fs::FileSystem arrow::Result> OpenInputFile(const std::string &path) override { + if (m_bAskedToClosed) + return arrow::Status::IOError( + "OpenInputFile(): file system in shutdown"); + std::string osPath(path); osPath += m_osQueryParameters; CPLDebugOnly(m_osEnvVarPrefix.c_str(), "Opening %s", osPath.c_str()); @@ -210,7 +260,13 @@ class VSIArrowFileSystem final : public arrow::fs::FileSystem if (fp == nullptr) return arrow::Status::IOError("OpenInputFile() failed for " + osPath); - return std::make_shared(std::move(fp)); + auto poFile = + std::make_shared(osPath, std::move(fp)); + { + std::lock_guard oLock(m_oMutex); + m_oSetFiles.emplace_back(path, poFile); + } + return poFile; } using arrow::fs::FileSystem::OpenOutputStream; diff --git a/ogr/ogrsf_frmts/parquet/ogr_parquet.h b/ogr/ogrsf_frmts/parquet/ogr_parquet.h index 9323f54bac2b..1b4fd97c6404 100644 --- a/ogr/ogrsf_frmts/parquet/ogr_parquet.h +++ b/ogr/ogrsf_frmts/parquet/ogr_parquet.h @@ -304,9 +304,12 @@ class OGRParquetDatasetLayer final : public OGRParquetLayerBase class OGRParquetDataset final : public OGRArrowDataset { + std::shared_ptr m_poFS{}; + public: explicit OGRParquetDataset( const std::shared_ptr &poMemoryPool); + ~OGRParquetDataset(); OGRLayer *ExecuteSQL(const char *pszSQLCommand, OGRGeometry *poSpatialFilter, @@ -314,6 +317,11 @@ class OGRParquetDataset final : public OGRArrowDataset void ReleaseResultSet(OGRLayer *poResultsSet) override; int TestCapability(const char *) override; + + void SetFileSystem(const std::shared_ptr &fs) + { + m_poFS = fs; + } }; /************************************************************************/ diff --git a/ogr/ogrsf_frmts/parquet/ogrparquetdataset.cpp b/ogr/ogrsf_frmts/parquet/ogrparquetdataset.cpp index a8d53bc139db..d0707cdacb09 100644 --- a/ogr/ogrsf_frmts/parquet/ogrparquetdataset.cpp +++ b/ogr/ogrsf_frmts/parquet/ogrparquetdataset.cpp @@ -32,6 +32,7 @@ #include "../arrow_common/ograrrowdataset.hpp" #include "../arrow_common/ograrrowlayer.hpp" +#include "../arrow_common/vsiarrowfilesystem.hpp" /************************************************************************/ /* OGRParquetDataset() */ @@ -43,6 +44,25 @@ OGRParquetDataset::OGRParquetDataset( { } +/************************************************************************/ +/* ~OGRParquetDataset() */ +/************************************************************************/ + +OGRParquetDataset::~OGRParquetDataset() +{ + // libarrow might continue to do I/O in auxiliary threads on the underlying + // files when using the arrow::dataset API even after we closed the dataset. + // This is annoying as it can cause crashes when closing GDAL, in particular + // the virtual file manager, as this could result in VSI files being + // accessed after their VSIVirtualFileSystem has been destroyed, resulting + // in crashes. The workaround is to make sure that VSIArrowFileSystem + // waits for all file handles it is aware of to have been destroyed. + close(); + auto poFS = std::dynamic_pointer_cast(m_poFS); + if (poFS) + poFS->AskToClose(); +} + /***********************************************************************/ /* ExecuteSQL() */ /***********************************************************************/ diff --git a/ogr/ogrsf_frmts/parquet/ogrparquetdriver.cpp b/ogr/ogrsf_frmts/parquet/ogrparquetdriver.cpp index aea3b32f3994..5237d2ba9cb1 100644 --- a/ogr/ogrsf_frmts/parquet/ogrparquetdriver.cpp +++ b/ogr/ogrsf_frmts/parquet/ogrparquetdriver.cpp @@ -51,7 +51,8 @@ static GDALDataset *OpenFromDatasetFactory( const std::string &osBasePath, const std::shared_ptr &factory, - CSLConstList papszOpenOptions) + CSLConstList papszOpenOptions, + const std::shared_ptr &fs) { std::shared_ptr dataset; PARQUET_ASSIGN_OR_THROW(dataset, factory->Finish()); @@ -65,6 +66,7 @@ static GDALDataset *OpenFromDatasetFactory( poDS.get(), CPLGetBasename(osBasePath.c_str()), bIsVSI, dataset, papszOpenOptions); poDS->SetLayer(std::move(poLayer)); + poDS->SetFileSystem(fs); return poDS.release(); } @@ -134,7 +136,7 @@ static GDALDataset *OpenParquetDatasetWithMetadata( std::make_shared(), std::move(options))); - return OpenFromDatasetFactory(osBasePath, factory, papszOpenOptions); + return OpenFromDatasetFactory(osBasePath, factory, papszOpenOptions, fs); } /************************************************************************/ @@ -182,7 +184,7 @@ OpenParquetDatasetWithoutMetadata(const std::string &osBasePathIn, std::move(options))); } - return OpenFromDatasetFactory(osBasePath, factory, papszOpenOptions); + return OpenFromDatasetFactory(osBasePath, factory, papszOpenOptions, fs); } #endif @@ -448,7 +450,8 @@ static GDALDataset *OGRParquetDriverOpen(GDALOpenInfo *poOpenInfo) if (fp == nullptr) return nullptr; } - infile = std::make_shared(std::move(fp)); + infile = std::make_shared(osFilename, + std::move(fp)); } else { diff --git a/port/cpl_vsi_virtual.h b/port/cpl_vsi_virtual.h index 5f5e01a0069b..b3fecdcb0027 100644 --- a/port/cpl_vsi_virtual.h +++ b/port/cpl_vsi_virtual.h @@ -139,6 +139,14 @@ struct CPL_DLL VSIVirtualHandle virtual size_t PRead(void *pBuffer, size_t nSize, vsi_l_offset nOffset) const; + /** Ask current operations to be interrupted. + * Implementations must be thread-safe, as this will typically be called + * from another thread than the active one for this file. + */ + virtual void Interrupt() + { + } + // NOTE: when adding new methods, besides the "actual" implementations, // also consider the VSICachedFile one. diff --git a/port/cpl_vsil_curl.cpp b/port/cpl_vsil_curl.cpp index 6988016c5d01..48c76ff69bdf 100644 --- a/port/cpl_vsil_curl.cpp +++ b/port/cpl_vsil_curl.cpp @@ -831,7 +831,8 @@ static GIntBig VSICurlGetExpiresFromS3LikeSignedURL(const char *pszURL) /* VSICURLMultiPerform() */ /************************************************************************/ -void VSICURLMultiPerform(CURLM *hCurlMultiHandle, CURL *hEasyHandle) +void VSICURLMultiPerform(CURLM *hCurlMultiHandle, CURL *hEasyHandle, + std::atomic *pbInterrupt) { int repeats = 0; @@ -866,6 +867,9 @@ void VSICURLMultiPerform(CURLM *hCurlMultiHandle, CURL *hEasyHandle) #endif CPLMultiPerformWait(hCurlMultiHandle, repeats); + + if (pbInterrupt && *pbInterrupt) + break; } CPLHTTPRestoreSigPipeHandler(old_handler); @@ -1150,7 +1154,7 @@ vsi_l_offset VSICurlHandle::GetFileSizeOrHeaders(bool bSetError, unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_FILETIME, 1); - VSICURLMultiPerform(hCurlMultiHandle, hCurlHandle); + VSICURLMultiPerform(hCurlMultiHandle, hCurlHandle, &m_bInterrupt); VSICURLResetHeaderAndWriterFunctions(hCurlHandle); @@ -1872,7 +1876,7 @@ std::string VSICurlHandle::DownloadRegion(const vsi_l_offset startOffset, unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_FILETIME, 1); - VSICURLMultiPerform(hCurlMultiHandle, hCurlHandle); + VSICURLMultiPerform(hCurlMultiHandle, hCurlHandle, &m_bInterrupt); VSICURLResetHeaderAndWriterFunctions(hCurlHandle); @@ -1880,10 +1884,13 @@ std::string VSICurlHandle::DownloadRegion(const vsi_l_offset startOffset, NetworkStatisticsLogger::LogGET(sWriteFuncData.nSize); - if (sWriteFuncData.bInterrupted) + if (sWriteFuncData.bInterrupted || m_bInterrupt) { bInterrupted = true; + // Notify that the download of the current region is finished + currentDownload.SetData(std::string()); + CPLFree(sWriteFuncData.pBuffer); CPLFree(sWriteFuncHeaderData.pBuffer); curl_easy_cleanup(hCurlHandle); @@ -3101,8 +3108,7 @@ size_t VSICurlHandle::PRead(void *pBuffer, size_t nSize, unchecked_curl_easy_setopt(hCurlHandle, CURLOPT_HTTPHEADER, headers); CURLM *hMultiHandle = poFS->GetCurlMultiHandleFor(osURL); - curl_multi_add_handle(hMultiHandle, hCurlHandle); - VSICURLMultiPerform(hMultiHandle); + VSICURLMultiPerform(hMultiHandle, hCurlHandle, &m_bInterrupt); { std::lock_guard oLock(m_oMutex); @@ -3125,9 +3131,12 @@ size_t VSICurlHandle::PRead(void *pBuffer, size_t nSize, if ((response_code != 206 && response_code != 225) || sWriteFuncData.nSize == 0) { - CPLDebug(poFS->GetDebugKey(), - "Request for %s failed with response_code=%ld", rangeStr, - response_code); + if (!m_bInterrupt) + { + CPLDebug(poFS->GetDebugKey(), + "Request for %s failed with response_code=%ld", rangeStr, + response_code); + } nRet = static_cast(-1); } else @@ -3137,7 +3146,6 @@ size_t VSICurlHandle::PRead(void *pBuffer, size_t nSize, memcpy(pBuffer, sWriteFuncData.pBuffer, nRet); } - curl_multi_remove_handle(hMultiHandle, hCurlHandle); VSICURLResetHeaderAndWriterFunctions(hCurlHandle); curl_easy_cleanup(hCurlHandle); CPLFree(sWriteFuncData.pBuffer); diff --git a/port/cpl_vsil_curl_class.h b/port/cpl_vsil_curl_class.h index 7691c4927f68..e371d497d4a0 100644 --- a/port/cpl_vsil_curl_class.h +++ b/port/cpl_vsil_curl_class.h @@ -42,6 +42,7 @@ #include "cpl_curl_priv.h" #include +#include #include #include #include @@ -411,6 +412,8 @@ class VSICurlHandle : public VSIVirtualHandle bool m_bUseHead = false; bool m_bUseRedirectURLIfNoQueryStringParams = false; + mutable std::atomic m_bInterrupt = false; + // Specific to Planetary Computer signing: // https://planetarycomputer.microsoft.com/docs/concepts/sas/ mutable bool m_bPlanetaryComputerURLSigning = false; @@ -497,6 +500,11 @@ class VSICurlHandle : public VSIVirtualHandle int Flush() override; int Close() override; + void Interrupt() override + { + m_bInterrupt = true; + } + bool HasPRead() const override { return true; @@ -1188,7 +1196,8 @@ void VSICURLInitWriteFuncStruct(cpl::WriteFuncStruct *psStruct, VSILFILE *fp, void *pReadCbkUserData); size_t VSICurlHandleWriteFunc(void *buffer, size_t count, size_t nmemb, void *req); -void VSICURLMultiPerform(CURLM *hCurlMultiHandle, CURL *hEasyHandle = nullptr); +void VSICURLMultiPerform(CURLM *hCurlMultiHandle, CURL *hEasyHandle = nullptr, + std::atomic *pbInterrupt = nullptr); void VSICURLResetHeaderAndWriterFunctions(CURL *hCurlHandle); int VSICurlParseUnixPermissions(const char *pszPermissions);