#! /usr/bin/env python """A script to clone tables from SQL database to Snowflake (by snapshot).""" import argparse from collections import defaultdict import copy import datetime import json import os import re import subprocess import time import boto3 from bin_scripts.config import BOTO3_CONFIG from snowflake_etl.flows.sql2sf.config import SourcesConf FLOW = 'sql2sf' SOURCE_QUERY_TYPE = 'snapshot' LOAD_STRATEGY = 'snapshot' sources = SourcesConf() def create_workflow_context(table, sfdb_params_override=None): """Create a context for a workflow. Args: table (tuple): db_host, schema, table_name, file_format. sfdb_params_override (dict): db and schema of SF destination. Returns: dict: The context of a workflow. """ source_db_host, source_schema, source_table, file_format = table db_type = sources.get_db_type(source_db_host) return dict( db_type=db_type, source_db_host=source_db_host, source_schema=source_schema, source_table=source_table, query_type=SOURCE_QUERY_TYPE, load_strategy=LOAD_STRATEGY, file_format=file_format, sfdb_params_override=sfdb_params_override) def exec_cmd(ctx): """Invocate garcon exec with params in a subprocess. Args: ctx (dict): The context of the flow. """ return subprocess.call(['garcon', 'exec', FLOW, '-c', json.dumps(ctx)]) def main(): """Main function.""" parser = argparse.ArgumentParser(description='Sync tables by snapshot') parser.add_argument( '--tables', help='Space separated list of the tables to sync', nargs='*') parser.add_argument( '--destination_sf_db_name', help='Name of the destination SF db') parser.add_argument( '--destination_sf_schema_name', help='Name of the destination SF schema') args = parser.parse_args() sfdb_params_from_args = { 'db': args.destination_sf_db_name, 'schema': args.destination_sf_schema_name} if args.tables: tables_to_sync = [ sources.get_table_props_by_name(table) for table in args.tables] else: tables_to_sync = sources.get_tables_to_sync() queue = copy.copy(tables_to_sync) while queue: table = queue.pop() # we must prevent multiple workflows from hitting SQL database # simultaneously, if source is not data warehouse (Redshift) source_db_host, source_schema, source_table, file_format = table db_type = sources.get_db_type(source_db_host) sfdb_params_override = {} if not sfdb_params_from_args.get('db'): # by default we want to override db name to use source schema (db) # name as db name in Snowflake, e.g. ART_RELATIONS # we also have to replace hyphen with underscore (because we have # names like stmt-db in MySQL) sfdb_params_override['db'] = source_schema.replace('-', '_') else: # but if args.destination_sf_db_name was specified, # we have to use it sfdb_params_override['db'] = sfdb_params_from_args['db'] # default SF schema in env var is PROD, and if # args.destination_sf_schema_name wasn't specified, it stays PROD, # otherwise it will be replaced by specified # args.destination_sf_schema_name sfdb_params_override['schema'] = args.destination_sf_schema_name if db_type == 'redshift': exec_cmd(create_workflow_context( table, sfdb_params_override=sfdb_params_from_args)) continue # do not perform checks, just proceed to the next table domain = '{env}_snowflake_etl'.format( env=os.environ.get('Environment')) active_executions = get_active_executions(domain) exec_infos = active_executions.get('executionInfos') or [] active_hosts = defaultdict(int) for execution_desc in exec_infos: current_workflow_id = execution_desc.get( 'execution').get('workflowId') workflow_type = execution_desc.get('workflowType').get('name') if workflow_type == 'sql2sf_snowflake_etl': current_table_name = re.findall( r'(\w+)(-snapshot)', current_workflow_id.split( '.')[1])[0][0] current_host = sources.get_table_props_by_name( current_table_name)[0] active_hosts[current_host] += 1 else: # do nothing if it is not sql2sqf worklow pass count = active_hosts.get(source_db_host) or 0 if count >= 3: print( '3 sql2sf workflows are already performing snapshot ' 'sync within db {host}, returning table {table} ' 'to queue\n'.format(host=source_db_host, table=source_table)) queue.insert(0, table) time.sleep(30) else: exec_cmd(create_workflow_context( table, sfdb_params_override=sfdb_params_override)) def get_active_executions(domain): client = boto3.client('swf', config=BOTO3_CONFIG) oldest_date = datetime.datetime.today() - datetime.timedelta(days=1) active_executions = client.list_open_workflow_executions( domain=domain, startTimeFilter={ 'oldestDate': datetime.datetime.combine( oldest_date, datetime.datetime.min.time()) }) return active_executions if __name__ == '__main__': main()