-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathCommonFlowUtility.py
More file actions
227 lines (188 loc) · 9 KB
/
Copy pathCommonFlowUtility.py
File metadata and controls
227 lines (188 loc) · 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# ---------------------------------------------------------------------------------------- #
# CommonFlowUtility.py #
# - Common functions for both traditional crawl framework and IntelligenceCrawler #
# ----------------------------------------------------------------------------------------- #
import logging
import urllib3
from logging import Logger
from GlobalConfig import DEFAULT_COLLECTOR_TOKEN
from IntelligenceCrawler.CrawlPipeline import format_exception_with_traceback
from IntelligenceHub import CollectedData
from PyLoggingBackend.LogUtility import get_tls_logger
from IntelligenceHubWebService import post_collected_intelligence
from Tools.ProcessCotrolException import ProcessSkip, ProcessProblem, ProcessIgnore
from IntelligenceCrawler.CrawlerGovernanceCore import GovernanceManager, CrawlSession
DEFAULT_CRAWL_ERROR_THRESHOLD = 3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# ------------------------------------------------------------------------------------------------------------------
import threading
from typing import Dict, Any, Optional, Tuple
import random
from collections import defaultdict
class CrawlCache:
def __init__(self):
self._lock = threading.Lock()
self._uncommit_content_cache: Dict[str, Any] = {}
def cache_len(self) -> int:
with self._lock:
return len(self._uncommit_content_cache)
def is_in_cache(self, url: str):
with self._lock:
return url in self._uncommit_content_cache
def cache_content(self, url: str, content: any):
with self._lock:
self._uncommit_content_cache[url] = content
def pop_content(self, url: str) -> Optional[Any]:
with self._lock:
return self._uncommit_content_cache.pop(url, None)
def pop_random_item(self) -> Optional[Tuple[str, Any]]:
with self._lock:
if self._uncommit_content_cache:
return self._uncommit_content_cache.popitem()
return '', None
def drop_cached_content(self, url: str):
with self._lock:
self._uncommit_content_cache.pop(url, None)
# ------------------------------------------------------------------------------------------------------------------
class PrefixLogger:
def __init__(self, logger: Logger, prefix):
self.logger = logger
self.prefix = prefix
def debug(self, message):
self.logger.debug(f"{self.prefix} {message}")
def info(self, message):
self.logger.info(f"{self.prefix} {message}")
def warning(self, message):
self.logger.warning(f"{self.prefix} {message}")
def error(self, message):
self.logger.error(f"{self.prefix} {message}")
def critical(self, message):
self.logger.critical(f"{self.prefix} {message}")
# ------------------------------------------------------------------------------------------------------------------
class CrawlContext:
def __init__(self,
flow_name: str,
i_hub_url: str,
collector_token: str,
crawler_governor: GovernanceManager,
error_threshold: int = DEFAULT_CRAWL_ERROR_THRESHOLD,
logger: Logger = None
):
self.flow_name = flow_name
self.i_hub_url = i_hub_url
self.crawler_governor = crawler_governor
self.collector_token = collector_token or DEFAULT_COLLECTOR_TOKEN
self.error_threshold = error_threshold
self.logger = PrefixLogger(logger or
get_tls_logger(__name__) or
logging.getLogger(__name__), f'[{flow_name}]:')
self.crawl_cache = CrawlCache()
self._submit_collected_data = post_collected_intelligence
def is_url_in_cache(self, url: str):
return self.crawl_cache.is_in_cache(url)
def check_get_cached_data(self, url: str)-> CollectedData:
return self.crawl_cache.pop_content(url)
def submit_collected_data(
self,
group: str,
collected_data: CollectedData,
cache_on_error: bool = True
):
collected_data.token = self.collector_token
if self._submit_collected_data:
self.logger.info(f"Submit collected data to: {self.i_hub_url}")
result = self._submit_collected_data(self.i_hub_url, collected_data, 10)
if result.get('status', 'success') == 'error':
if cache_on_error:
# Only cache on submission error.
self.crawl_cache.cache_content(
collected_data.informant, (collected_data, group) # <- Cached data is packed here.
)
raise CrawlSession.Cached('commit_error')
else:
self.logger.warning(f'no method to submit collected data, data dropped.')
self.logger.debug(f'Article finished.')
def submit_cached_data(self, limit: int = -1):
count = 0
while (limit < 0) or (count < limit):
url, content = self.crawl_cache.pop_random_item()
if not content:
break
collected_data, group = content # <- Cached data is unpacked here.
with self.crawler_governor.transaction(url, group) as task:
try:
self.submit_collected_data(group, collected_data)
task.success(state_msg='Cached data submitted.')
except CrawlSession.Flow:
raise # Handle by context
except Exception as e:
self.handle_process_exception(task, e)
finally:
count += 1
if count:
self.logger.info(f"Process cached data for {self.flow_name}, count: {count}.")
def handle_process_exception(self, task: CrawlSession, e: Exception):
if isinstance(e, ProcessSkip):
task.skip(e.reason)
self.logger.debug('Article skipped.')
elif isinstance(e, ProcessIgnore):
task.ignore()
self.logger.debug('Article ignored.')
elif isinstance(e, ProcessProblem):
if e.problem == 'fetch_error':
task.fail_temp(state_msg='Fetch error')
elif e.problem in ['commit_error']:
# Just ignore because there will be a retry at next loop.
task.cached()
else:
task.fail_perm(state_msg=f"Task {task.group_path} got unexpected ProcessProblem reason: {e.problem}")
else:
task.fail_perm(state_msg=str(e))
self.logger.error(f"Task {task.group_path} got unexpected exception: {str(e)}")
print(format_exception_with_traceback(e))
# def check_raise_url_status(self, article_link: str, crawl_record: CrawlRecord, levels: str | List[str] = ''):
# """
# This function returns nothing. If everything is OK, this function will pass through otherwise raise exceptions.
# :param article_link: The link to be checked.
# :param crawl_record: The crawl record instance.
# :param levels: Levels of logging and record.
# :return: None
# """
# full_levels = self._full_levels(levels)
# url_status = crawl_record.get_url_status(article_link, from_db=False)
#
# if url_status >= STATUS_SUCCESS:
# raise ProcessSkip('already exists', article_link, leveling=full_levels)
# elif url_status <= STATUS_UNKNOWN:
# pass # <- Process going on here
# elif url_status == STATUS_ERROR:
# url_error_count = crawl_record.get_error_count(article_link, from_db=False)
# if url_error_count < 0:
# raise ProcessProblem('db_error', article_link, leveling=full_levels)
# if url_error_count >= self.error_threshold:
# raise ProcessSkip('max retry exceed', article_link, leveling=full_levels)
# else:
# pass # <- Process going on here
# else: # STATUS_DB_ERROR
# raise ProcessProblem('db_error', article_link, leveling=full_levels)
#
# # ----- Also keep old mechanism checking to make it compatible -----
# if has_url(article_link):
# raise ProcessSkip('already exists', article_link, leveling=full_levels)
# @staticmethod
# def wait_interruptibly(total_duration_s: int, stop_event: threading.Event) -> bool:
# """
# Waits for the specified duration while periodically checking for the stop_event.
#
# Returns True if the full duration was reached, False if the event was set early.
# """
# remaining = total_duration_s
#
# CHECK_INTERVAL_S = 5
#
# while remaining > 0 and not stop_event.is_set():
# sleep_time = min(CHECK_INTERVAL_S, remaining)
# time.sleep(sleep_time)
# remaining -= sleep_time
#
# return remaining <= 0