import os
import pymongo
from urllib.parse import urlsplit
from mspasspy.db.client import DBClient
from mspasspy.util.db_utils import MongoDBWorker
from mspasspy.db.database import Database
from mspasspy.global_history.manager import GlobalHistoryManager
try:
from pyspark import SparkConf, SparkContext
from pyspark.sql import SparkSession
except Exception as err:
SparkConf = None
SparkContext = None
SparkSession = None
_mspasspy_has_pyspark = False
_mspasspy_pyspark_import_error = err
else:
_mspasspy_has_pyspark = True
_mspasspy_pyspark_import_error = None
try:
from dask.distributed import Client as DaskClient
_mspasspy_has_dask_distributed = True
except ImportError:
_mspasspy_has_dask_distributed = False
from mspasspy.ccore.utility import MsPASSError
def _require_pyspark():
if _mspasspy_has_pyspark:
return
message = "Spark scheduler was requested, but PySpark could not be imported"
if _mspasspy_pyspark_import_error is not None:
message += ": " + str(_mspasspy_pyspark_import_error)
raise MsPASSError(message + ".", "Fatal")
def _address_has_port(address):
if "://" in address:
parsed_address = urlsplit(address)
else:
parsed_address = urlsplit("//" + address)
try:
return parsed_address.port is not None
except ValueError:
return False
def _build_dask_scheduler_address(scheduler_address, scheduler_port=None):
if _address_has_port(scheduler_address):
return scheduler_address
if scheduler_port is None or scheduler_port == "":
scheduler_port = "8786"
return scheduler_address + ":" + str(scheduler_port)
[docs]
class Client:
"""
A client-side representation of MSPASS.
This is the only client users should use in MSPASS. The client manages all the other clients or instances.
It creates and manages a Database client.
It creates and manages a Global Hisotry Manager.
It creates and manages a scheduler(spark/dask)
For the address and port of each client/instances, we first check the user specified parameters, if not then
serach the environment varibales values, if not againm then use the default settings.
An existing :class:`dask.distributed.Client` can be supplied with ``dask_client``.
This is useful for externally managed Dask clusters, including Dask Gateway
clusters. The caller owns the external cluster and should keep it alive while
the MsPASS client is using it.
"""
def __init__(
self,
database_host=None,
scheduler=None,
scheduler_host=None,
job_name="mspass",
database_name="mspass",
schema=None,
collection=None,
dask_client=None,
):
# job_name should be a string
if database_host is not None and not type(database_host) is str:
raise MsPASSError(
"database_host should be a string but "
+ str(type(database_host))
+ " is found.",
"Fatal",
)
if scheduler is not None and scheduler not in ("dask", "spark", "none"):
raise MsPASSError(
"scheduler should be dask, spark, or none but "
+ str(scheduler)
+ " is found.",
"Fatal",
)
if scheduler_host is not None and not type(scheduler_host) is str:
raise MsPASSError(
"scheduler_host should be a string but "
+ str(type(scheduler_host))
+ " is found.",
"Fatal",
)
if job_name is not None and not type(job_name) is str:
raise MsPASSError(
"job_name should be a string but " + str(type(job_name)) + " is found.",
"Fatal",
)
if database_name is not None and not type(database_name) is str:
raise MsPASSError(
"database_name should be a string but "
+ str(type(database_name))
+ " is found.",
"Fatal",
)
# collection should be a string
if collection is not None and type(collection) is not str:
raise MsPASSError(
"collection should be a string but "
+ str(type(collection))
+ " is found.",
"Fatal",
)
if dask_client is not None:
if scheduler == "none":
raise MsPASSError(
"dask_client cannot be used when scheduler is none.",
"Fatal",
)
if scheduler == "spark":
raise MsPASSError(
"dask_client can only be used with the dask scheduler.",
"Fatal",
)
if not _mspasspy_has_dask_distributed or not isinstance(
dask_client, DaskClient
):
raise MsPASSError(
"dask_client should be a dask.distributed.Client but "
+ str(type(dask_client))
+ " is found.",
"Fatal",
)
# check env variables
MSPASS_DB_ADDRESS = os.environ.get("MSPASS_DB_ADDRESS")
MONGODB_PORT = os.environ.get("MONGODB_PORT")
MSPASS_SCHEDULER = os.environ.get("MSPASS_SCHEDULER")
MSPASS_SCHEDULER_ADDRESS = os.environ.get("MSPASS_SCHEDULER_ADDRESS")
DASK_SCHEDULER_PORT = os.environ.get("DASK_SCHEDULER_PORT")
SPARK_MASTER_PORT = os.environ.get("SPARK_MASTER_PORT")
if (
dask_client is None
and not _mspasspy_has_pyspark
and (
scheduler == "spark"
or (scheduler is None and MSPASS_SCHEDULER == "spark")
)
):
_require_pyspark()
# create a database client
# priority: parameter -> env -> default
database_host_has_port = False
if database_host:
database_address = database_host
# check if database_host contains port number already
if ":" in database_address:
database_host_has_port = True
elif MSPASS_DB_ADDRESS:
database_address = MSPASS_DB_ADDRESS
else:
database_address = "127.0.0.1"
# add port
if not database_host_has_port and MONGODB_PORT:
database_address += ":" + MONGODB_PORT
try:
self._db_client = DBClient(database_address)
self._db_client.server_info()
except Exception as err:
raise MsPASSError(
"Runntime error: cannot create a database client with: "
+ database_address,
"Fatal",
)
# set default database name
self._default_database_name = database_name
self._default_schema = schema
self._default_collection = collection
# create a Global History Manager
if schema:
global_history_manager_db = Database(
self._db_client, database_name, db_schema=schema
)
else:
global_history_manager_db = Database(self._db_client, database_name)
self._global_history_manager = GlobalHistoryManager(
global_history_manager_db, job_name, collection=collection
)
# set scheduler
self._scheduler_disabled = False
if dask_client is not None:
self._scheduler = "dask"
elif scheduler:
if scheduler == "none":
self._scheduler = None
self._scheduler_disabled = True
else:
self._scheduler = scheduler
elif MSPASS_SCHEDULER:
if MSPASS_SCHEDULER not in ("dask", "spark", "none"):
raise MsPASSError(
"MSPASS_SCHEDULER should be dask, spark, or none but "
+ str(MSPASS_SCHEDULER)
+ " is found.",
"Fatal",
)
if MSPASS_SCHEDULER == "none":
self._scheduler = None
self._scheduler_disabled = True
else:
self._scheduler = MSPASS_SCHEDULER
else:
if _mspasspy_has_dask_distributed:
self._scheduler = "dask"
elif _mspasspy_has_pyspark:
self._scheduler = "spark"
else:
self._scheduler = None
# scheduler configuration
if self._scheduler == "spark":
scheduler_host_has_port = False
if scheduler_host:
self._spark_master_url = scheduler_host
# add spark:// prefix if not exist
if "spark://" not in scheduler_host:
self._spark_master_url = "spark://" + self._spark_master_url
# check if spark host address contains port number already
if self._spark_master_url.count(":") == 2:
scheduler_host_has_port = True
elif MSPASS_SCHEDULER_ADDRESS:
self._spark_master_url = MSPASS_SCHEDULER_ADDRESS
# add spark:// prefix if not exist
if "spark://" not in MSPASS_SCHEDULER_ADDRESS:
self._spark_master_url = "spark://" + self._spark_master_url
else:
self._spark_master_url = "local"
# add port number
# 1. not the default 'local'
# 2. scheduler_host and does not contain port number
# 3. SPARK_MASTER_PORT exists
if (
(scheduler_host or MSPASS_SCHEDULER_ADDRESS)
and not scheduler_host_has_port
and SPARK_MASTER_PORT
):
self._spark_master_url += ":" + SPARK_MASTER_PORT
# sanity check
try:
spark = (
SparkSession.builder.appName("mspass")
.master(self._spark_master_url)
.getOrCreate()
)
self._spark_context = spark.sparkContext
except Exception as err:
raise MsPASSError(
"Runntime error: cannot create a spark configuration with: "
+ self._spark_master_url,
"Fatal",
)
elif self._scheduler == "dask":
# if no defind scheduler_host and no MSPASS_SCHEDULER_ADDRESS, use local cluster to create a client
if dask_client is not None:
self._dask_client = dask_client
elif not scheduler_host and not MSPASS_SCHEDULER_ADDRESS:
self._dask_client = DaskClient()
else:
if scheduler_host:
scheduler_address = scheduler_host
else:
scheduler_address = MSPASS_SCHEDULER_ADDRESS
self._dask_client_address = _build_dask_scheduler_address(
scheduler_address, DASK_SCHEDULER_PORT
)
# sanity check
try:
self._dask_client = DaskClient(self._dask_client_address)
except Exception as err:
raise MsPASSError(
"Runntime error: cannot create a dask client with: "
+ self._dask_client_address,
"Fatal",
)
elif not self._scheduler_disabled:
print("There is no spark or dask installed, this client has no scheduler")
# Auto-register MongoDB worker plugin for dask to avoid DB serialization leaks
if self._scheduler == "dask":
mongo_plugin = MongoDBWorker(self, dbclient_key="dbclient")
self._dask_client.register_plugin(mongo_plugin, name="mongodb_worker")
[docs]
def get_database_client(self):
"""
Get the database client in the global history manager
:return: :class:`mspasspy.db.database.Database`
"""
return self._db_client
[docs]
def get_database(self, database_name=None):
"""
Get a database by database_name, if database_name is not specified, use the default one
:param database_name: the name of database
:type database_name: :class:`str`
:return: :class:`mspasspy.db.database.Database`
"""
if not database_name:
return Database(self._db_client, self._default_database_name)
return Database(self._db_client, database_name)
[docs]
def get_global_history_manager(self):
"""
Get the global history manager with this client
:return: :class:`mspasspy.global_history.manager.GlobalHistoryManager`
"""
return self._global_history_manager
[docs]
def get_scheduler(self):
"""
Get the scheduler(spark/dask) with this client
:return: :class:`pyspark.SparkContext`/:class:`dask.distributed.Client`/None
"""
if self._scheduler == "spark":
return self._spark_context
elif self._scheduler == "dask":
return self._dask_client
elif self._scheduler_disabled:
return None
else:
print(
"There is no spark or dask installed, this client has no scheduler, returned None"
)
return None
[docs]
def set_database_client(self, database_host, database_port=None):
"""
Set a database client by database_host(and database_port)
:param database_host: the host address of database client
:type database_host: :class:`str`
:param database_port: the port of database client
:type database_port: :class:`str`
"""
database_host_has_port = False
database_address = database_host
# check if port is already in the database_host address
if ":" in database_host:
database_host_has_port = True
# add port
if not database_host_has_port and database_port:
database_address += ":" + database_port
# sanity check
temp_db_client = self._db_client
try:
self._db_client = DBClient(database_address)
self._db_client.server_info()
except Exception as err:
# restore the _db_client
self._db_client = temp_db_client
raise MsPASSError(
"Runntime error: cannot create a database client with: "
+ database_address,
"Fatal",
)
[docs]
def set_global_history_manager(self, history_db, job_name, collection=None):
"""
Set a global history manager by history_db, job_name(and collection)
:param history_db: the database will be set in the global history manager
:type history_db: :class:`mspasspy.db.database.Database`
:param job_name: the job name will be set in the global history manager
:type job_name: :class:`str`
:param collection: the collection name will be set in the history_db
:type collection: :class:`str`
"""
if not isinstance(history_db, Database):
raise TypeError(
"history_db should be a mspasspy.db.Database but "
+ str(type(history_db))
+ " is found."
)
if not type(job_name) is str:
raise TypeError(
"job_name should be a string but " + str(type(job_name)) + " is found."
)
if collection is not None and type(collection) is not str:
raise TypeError(
"collection should be a string but "
+ str(type(collection))
+ " is found."
)
self._global_history_manager = GlobalHistoryManager(
history_db, job_name, collection=collection
)
[docs]
def set_scheduler(self, scheduler, scheduler_host, scheduler_port=None):
"""
Set a scheduler by scheduler type, scheduler_host(and scheduler_port)
:param scheduler: the scheduler type, should be either dask or spark
:type scheduler: :class:`str`
:param scheduler_host: the host address of scheduler
:type scheduler_host: :class:`str`
:param scheduler_port: the port of scheduler
:type scheduler_port: :class:`str`
"""
if scheduler != "dask" and scheduler != "spark":
raise MsPASSError(
"scheduler should be either dask or spark but "
+ str(scheduler)
+ " is found.",
"Fatal",
)
if scheduler == "spark":
_require_pyspark()
prev_scheduler = self._scheduler
self._scheduler = scheduler
if scheduler == "spark":
scheduler_host_has_port = False
self._spark_master_url = scheduler_host
# add spark:// prefix if not exist
if "spark://" not in scheduler_host:
self._spark_master_url = "spark://" + self._spark_master_url
# check if spark host address contains port number already
if self._spark_master_url.count(":") == 2:
scheduler_host_has_port = True
# add port
if not scheduler_host_has_port and scheduler_port:
self._spark_master_url += ":" + scheduler_port
# sanity check
prev_spark_context = None
prev_spark_conf = None
if hasattr(self, "_spark_context"):
prev_spark_context = self._spark_context
prev_spark_conf = self._spark_context.getConf()
try:
if hasattr(self, "_spark_context") and isinstance(
self._spark_context, SparkContext
):
# update the confinguration
spark_conf = self._spark_context._conf.setMaster(
self._spark_master_url
)
else:
spark_conf = (
SparkConf()
.setAppName("mspass")
.setMaster(self._spark_master_url)
)
# stop the previous spark context
# FIXME if the new context does not start, we shouldn't stop the previous here.
# if prev_spark_context:
# prev_spark_context.stop()
# create a new spark context -> might cause error so that execute exception code
spark = SparkSession.builder.config(conf=spark_conf).getOrCreate()
self._spark_context = spark.sparkContext
except Exception as err:
# restore the spark context by the previous spark configuration
if prev_spark_conf:
self._spark_context = SparkContext.getOrCreate(conf=prev_spark_conf)
# restore the scheduler type
if self._scheduler == "spark" and prev_scheduler == "dask":
self._scheduler = prev_scheduler
raise MsPASSError(
"Runntime error: cannot create a spark configuration with: "
+ self._spark_master_url,
"Fatal",
)
# close previous dask client if success
if hasattr(self, "_dask_client"):
del self._dask_client
elif scheduler == "dask":
self._dask_client_address = _build_dask_scheduler_address(
scheduler_host, scheduler_port
)
# sanity check
prev_dask_client = None
if hasattr(self, "_dask_client"):
prev_dask_client = self._dask_client
try:
# create a new dask client
self._dask_client = DaskClient(self._dask_client_address)
except Exception as err:
# restore the dask client if exists
if prev_dask_client:
self._dask_client = prev_dask_client
# restore the scheduler type
if self._scheduler == "dask" and prev_scheduler == "spark":
self._scheduler = prev_scheduler
raise MsPASSError(
"Runntime error: cannot create a dask client with: "
+ self._dask_client_address,
"Fatal",
)
# remove previous spark context if success setting new dask client
if hasattr(self, "_spark_context"):
del self._spark_context