non graph solution but extremely memory intensive

This commit is contained in:
nitowa
2022-08-27 12:15:25 -04:00
parent 562a281ce4
commit 183723e46f
2 changed files with 296 additions and 106 deletions
+136 -103
View File
@@ -1,5 +1,6 @@
from gc import collect from gc import collect
import json import json
from select import select
from sqlite3 import Row from sqlite3 import Row
from typing import Iterable, List from typing import Iterable, List
@@ -14,6 +15,7 @@ start = time.time()
config = json.load(open("./settings.json")) config = json.load(open("./settings.json"))
debug = config['debug'] debug = config['debug']
class Master: class Master:
spark: SparkSession spark: SparkSession
CLUSTERS_TABLE: str CLUSTERS_TABLE: str
@@ -25,133 +27,164 @@ class Master:
self.CLUSTERS_TABLE = f"{config['cassandra_catalog']}.{config['cassandra_keyspace']}.{config['clusters_table_name']}" self.CLUSTERS_TABLE = f"{config['cassandra_catalog']}.{config['cassandra_keyspace']}.{config['clusters_table_name']}"
self.TX_TABLE = f"{config['cassandra_catalog']}.{config['cassandra_keyspace']}.{config['tx_table_name']}" self.TX_TABLE = f"{config['cassandra_catalog']}.{config['cassandra_keyspace']}.{config['tx_table_name']}"
def makeSparkContext(self,config) -> SparkSession: def makeSparkContext(self, config) -> SparkSession:
return SparkSession.builder \ return SparkSession.builder \
.appName('SparkCassandraApp') \ .appName('SparkCassandraApp') \
.config(f"spark.sql.catalog.{config['cassandra_catalog']}", "com.datastax.spark.connector.datasource.CassandraCatalog") \ .config(f"spark.sql.catalog.{config['cassandra_catalog']}", "com.datastax.spark.connector.datasource.CassandraCatalog") \
.getOrCreate() .getOrCreate()
def group_tx_addrs(self) -> DataFrame: def get_tx_dataframe(self) -> DataFrame:
return self.spark \ return self.spark.table(self.TX_TABLE)
.read \
.table(self.TX_TABLE) \
.groupBy("tx_id") \
.agg(F.collect_set('address').alias('addresses'))
def group_cluster_addrs(self) -> DataFrame: def union_single_col(self, df1: DataFrame, df2: DataFrame, column: str) -> DataFrame:
return self.spark \ return df1 \
.read \ .select(column) \
.table(self.CLUSTERS_TABLE) \ .union(df2.select(column))
.groupBy("id") \
.agg(F.collect_set('address').alias('addresses'))
def insertNewCluster (self, addrs: Iterable[str], root: str | None = None) -> str: def reduce_concat_array_column(self, df: DataFrame, column: str, distinct:bool = False) -> DataFrame:
if(root == None): df = self.explode_array_col(df.select(column), column)
root = addrs[0] return self.collect_col_to_array(df, column, distinct)
df = self.spark.createDataFrame(map(lambda addr: (addr, root), addrs), schema=['address', 'id'])
df.writeTo(self.CLUSTERS_TABLE).append()
return root
def enumerate(self, data: DataFrame) -> DataFrame: def collect_col_to_array(self, df: DataFrame, column: str, distinct: bool = False) -> DataFrame:
return data \ if(distinct):
return df.select(F.collect_set(column).alias(column))
else:
return df.select(F.collect_list(column).alias(column))
def explode_array_col(self, df: DataFrame, column: str) -> DataFrame:
return df \
.rdd \ .rdd \
.zipWithIndex() \ .flatMap(lambda row: list(map(lambda elem: (elem,), row[column]))) \
.toDF(["tx_group", "index"]) .toDF([column])
def rewrite_cluster_id(self, cluster_roots: Iterable[str], new_cluster_root: str) -> None: def array_col_to_elements(self, df: DataFrame, column: str, distinct:bool = False) -> DataFrame:
cluster_rewrite = self.spark \ exploded = master.explode_array_col(
.table(self.CLUSTERS_TABLE) \ df,
.where(F.col('id').isin(cluster_roots)) \ column
.select('address') \ )
.rdd \
.map(lambda addr: (addr['address'], new_cluster_root)) \
.toDF(['address', 'id']) \
if(debug): #this is likely redundant
print("REWRITE JOB") collected = master.collect_col_to_array(
cluster_rewrite.show(truncate=False, vertical=True) exploded,
print() column,
distinct
)
return self.explode_array_col(
collected,
column
)
cluster_rewrite.writeTo(self.CLUSTERS_TABLE).append()
# end class Master # end class Master
"""
tuple structure:
Row => Row(id=addr, addresses=list[addr] | the cluster
Iterable[str] => list[addr] | the transaction addresses
"""
def find(data: tuple[Row, Iterable[str]]) -> str | None:
cluster = data[0]
tx = data[1]
clusteraddresses = cluster['addresses'] + [cluster['id']]
if any(x in tx for x in clusteraddresses):
return cluster['id']
else:
return None
master = Master(config) master = Master(config)
tx_df = master.get_tx_dataframe()
tx_addr_groups = master.group_tx_addrs()
tx_groups_indexed = master.enumerate(tx_addr_groups).cache()
for i in range(0, tx_addr_groups.count()): #Turn transactions into a list of ('id', [addr, addr, ...])
cluster_addr_groups = master.group_cluster_addrs() tx_grouped = tx_df \
.groupBy('tx_id') \
if(debug): .agg(F.collect_set('address').alias('addresses')) \
print("KNOWN CLUSTERS")
cluster_addr_groups.show(truncate=True)
print()
tx_addrs: Iterable[str] = tx_groups_indexed \
.where(tx_groups_indexed.index == i) \
.select('tx_group') \
.collect()[0]['tx_group']['addresses']
if(debug):
print("CURRENT TX")
print(tx_addrs)
print()
if (cluster_addr_groups.count() == 0):
master.insertNewCluster(tx_addrs)
continue
cluster_tx_mapping = cluster_addr_groups \
.rdd \ .rdd \
.map(lambda cluster: (cluster, tx_addrs)) .zipWithIndex() \
.toDF(['tx', 'index']) \
if(debug): .select(
print("cluster_tx_mapping") F.col('tx.tx_id').alias('tx_id'),
cluster_tx_mapping \ F.col('tx.addresses').alias('addresses'),
.toDF(['cluster', 'tx']) \ 'index'
.show(truncate=True) ) \
print() .cache()
matched_roots: "List[str]" = cluster_tx_mapping \ # TODO: Load clusters from DB, check if any exist, if no make initial cluster, else proceed with loaded data
.map(find) \
.filter(lambda root: root != None) \
.collect()
if(debug): # find initial cluster
print("FOUND ROOTS")
print(matched_roots) # take the first tx
print() tx_zero = tx_grouped \
.select(tx_grouped.tx_id, tx_grouped.addresses) \
.where(tx_grouped.index == 0)
# find txs with overlapping addresses
overlapping_txs = tx_grouped \
.where((tx_grouped.index != 0)) \
.join(tx_zero.withColumnRenamed('addresses', 'tx_addresses')) \
.select(
tx_grouped.index,
tx_grouped.addresses,
F.arrays_overlap(tx_grouped.addresses, 'tx_addresses').alias('overlap')
) \
.where(F.col('overlap') == True) \
# overlapped txs must not be considered anymore, so remove them candidate dataframe
tx_grouped = tx_grouped \
.join(overlapping_txs, 'index', 'leftanti') \
.filter(tx_grouped.index != 0)
# get the distinct addresses of all overlaps in a single array
distinct_addresses = master.reduce_concat_array_column(
master.union_single_col(
overlapping_txs, tx_zero, column='addresses'
),
column='addresses',
distinct=True,
)
#pick out a random representative for this cluster and add it to every address
cluster = distinct_addresses \
.rdd \
.flatMap(lambda row: list(map(lambda addr: (addr, row['addresses'][0]), row['addresses']))) \
.toDF(['address', 'id'])
# done finding initial cluster
#group cluster by representative and transform the result into a list of shape ('id', ['addr', 'addr', ...])
clusters_grouped = cluster \
.groupBy('id') \
.agg(F.collect_list('address').alias('addresses'))
def take_tx_and_cluster(txs: DataFrame, clusters: DataFrame):
if (txs.count() == 0): # done!
return clusters
# take a random tx
tx = txs \
.select('*').limit(1)
# find clusters with overlapping addresses from tx
overlapping_clusters = clusters \
.join(tx.withColumnRenamed('addresses', 'tx_addresses')) \
.select(
clusters.id,
clusters.addresses,
F.arrays_overlap(clusters.addresses,'tx_addresses').alias('overlap')
) \
.where(F.col('overlap') == True)
#collect all addresses into single array field
new_cluster_arr = master.reduce_concat_array_column(
master.union_single_col(tx, overlapping_clusters, 'addresses'),
column='addresses',
distinct=True
)
#declare cluster representative
new_cluster = new_cluster_arr \
.rdd \
.flatMap(lambda row: list(map(lambda addr: (addr, row['addresses'][0]), row['addresses']))) \
.toDF(['address', 'id']) \
.groupBy('id') \
.agg(F.collect_list('address').alias('addresses'))
#start new round with txs minus the one just used, and updated clusters
return take_tx_and_cluster(
txs.join(tx, 'index', 'leftanti'),
clusters.join(overlapping_clusters, 'id', 'leftanti').union(new_cluster)
)
if(len(matched_roots) == 0): take_tx_and_cluster(tx_grouped, clusters_grouped).show()
master.insertNewCluster(tx_addrs)
elif(len(matched_roots) == 1):
master.insertNewCluster(tx_addrs, matched_roots[0])
else:
master.rewrite_cluster_id(matched_roots[1:], matched_roots[0])
master.insertNewCluster(tx_addrs, matched_roots[0])
if(debug):
print("======================================================================")
end = time.time() end = time.time()
print("ELAPSED TIME:", end-start) print("ELAPSED TIME:", end-start)
+157
View File
@@ -0,0 +1,157 @@
from gc import collect
import json
from sqlite3 import Row
from typing import Iterable, List
from pyspark.sql import SparkSession, DataFrame, Row
from pyspark.sql import functions as F
import time
start = time.time()
config = json.load(open("./settings.json"))
debug = config['debug']
class Master:
spark: SparkSession
CLUSTERS_TABLE: str
TX_TABLE: str
def __init__(self, config):
self.spark = self.makeSparkContext(config)
self.config = config
self.CLUSTERS_TABLE = f"{config['cassandra_catalog']}.{config['cassandra_keyspace']}.{config['clusters_table_name']}"
self.TX_TABLE = f"{config['cassandra_catalog']}.{config['cassandra_keyspace']}.{config['tx_table_name']}"
def makeSparkContext(self,config) -> SparkSession:
return SparkSession.builder \
.appName('SparkCassandraApp') \
.config(f"spark.sql.catalog.{config['cassandra_catalog']}", "com.datastax.spark.connector.datasource.CassandraCatalog") \
.getOrCreate()
def group_tx_addrs(self) -> DataFrame:
return self.spark \
.read \
.table(self.TX_TABLE) \
.groupBy("tx_id") \
.agg(F.collect_set('address').alias('addresses'))
def group_cluster_addrs(self) -> DataFrame:
return self.spark \
.read \
.table(self.CLUSTERS_TABLE) \
.groupBy("id") \
.agg(F.collect_set('address').alias('addresses'))
def insertNewCluster (self, addrs: Iterable[str], root: str | None = None) -> str:
if(root == None):
root = addrs[0]
df = self.spark.createDataFrame(map(lambda addr: (addr, root), addrs), schema=['address', 'id'])
df.writeTo(self.CLUSTERS_TABLE).append()
return root
def enumerate(self, data: DataFrame) -> DataFrame:
return data \
.rdd \
.zipWithIndex() \
.toDF(["tx_group", "index"])
def rewrite_cluster_id(self, cluster_roots: Iterable[str], new_cluster_root: str) -> None:
cluster_rewrite = self.spark \
.table(self.CLUSTERS_TABLE) \
.where(F.col('id').isin(cluster_roots)) \
.select('address') \
.rdd \
.map(lambda addr: (addr['address'], new_cluster_root)) \
.toDF(['address', 'id']) \
if(debug):
print("REWRITE JOB")
cluster_rewrite.show(truncate=False, vertical=True)
print()
cluster_rewrite.writeTo(self.CLUSTERS_TABLE).append()
# end class Master
"""
tuple structure:
Row => Row(id=addr, addresses=list[addr] | the cluster
Iterable[str] => list[addr] | the transaction addresses
"""
def find(data: tuple[Row, Iterable[str]]) -> str | None:
cluster = data[0]
tx = data[1]
clusteraddresses = cluster['addresses'] + [cluster['id']]
if any(x in tx for x in clusteraddresses):
return cluster['id']
else:
return None
master = Master(config)
tx_addr_groups = master.group_tx_addrs()
tx_groups_indexed = master.enumerate(tx_addr_groups).cache()
for i in range(0, tx_addr_groups.count()):
cluster_addr_groups = master.group_cluster_addrs()
if(debug):
print("KNOWN CLUSTERS")
cluster_addr_groups.show(truncate=True)
print()
tx_addrs: Iterable[str] = tx_groups_indexed \
.where(tx_groups_indexed.index == i) \
.select('tx_group') \
.collect()[0]['tx_group']['addresses']
if(debug):
print("CURRENT TX")
print(tx_addrs)
print()
if (cluster_addr_groups.count() == 0):
master.insertNewCluster(tx_addrs)
continue
cluster_tx_mapping = cluster_addr_groups \
.rdd \
.map(lambda cluster: (cluster, tx_addrs))
if(debug):
print("cluster_tx_mapping")
cluster_tx_mapping \
.toDF(['cluster', 'tx']) \
.show(truncate=True)
print()
matched_roots: "List[str]" = cluster_tx_mapping \
.map(find) \
.filter(lambda root: root != None) \
.collect()
if(debug):
print("FOUND ROOTS")
print(matched_roots)
print()
if(len(matched_roots) == 0):
master.insertNewCluster(tx_addrs)
elif(len(matched_roots) == 1):
master.insertNewCluster(tx_addrs, matched_roots[0])
else:
master.rewrite_cluster_id(matched_roots[1:], matched_roots[0])
master.insertNewCluster(tx_addrs, matched_roots[0])
if(debug):
print("======================================================================")
end = time.time()
print("ELAPSED TIME:", end-start)