blob: 7a1d5a47dda5ff3eeb79e5a03d5e3f9d550a9f4e [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
import atexit
import functools
import os
import sys
import time
from queue import Empty, Full, Queue
from threading import Event, Thread
from typing import TYPE_CHECKING, Callable, Optional
from skywalking import config, loggings, meter, plugins, profile, sampling
from skywalking.agent.protocol import Protocol, ProtocolAsync
from skywalking.command import command_service, command_service_async
from skywalking.loggings import logger
from skywalking.profile.profile_task import ProfileTask
from skywalking.profile.snapshot import TracingThreadSnapshot
from skywalking.protocol.language_agent.Meter_pb2 import MeterData
from skywalking.protocol.logging.Logging_pb2 import LogData
from skywalking.utils.reporter_log import log_dropped_throttled, log_reporter_exception_throttled
from skywalking.utils.singleton import Singleton
if TYPE_CHECKING:
from skywalking.trace.context import Segment
if config.agent_asyncio_enhancement:
import uvloop
uvloop.install()
# Shutdown must never block the host indefinitely (Node flush budget parity).
_SHUTDOWN_FLUSH_TIMEOUT_SEC = 2.0
_SHUTDOWN_JOIN_TIMEOUT_SEC = 2.0
_SHUTDOWN_LOOP_JOIN_TIMEOUT_SEC = 3.0
def _abandon_sync_queue(q: Queue) -> int:
"""Drop queued items and balance unfinished_tasks so Queue.join() can finish."""
abandoned = 0
while True:
try:
q.get_nowait()
q.task_done()
abandoned += 1
except Empty:
break
return abandoned
def _join_sync_queue(q: Queue, timeout: float) -> bool:
"""Return True if join completed within timeout."""
done = Event()
def _wait():
q.join()
done.set()
Thread(target=_wait, name='sw-queue-join', daemon=True).start()
return done.wait(timeout)
def _shutdown_sync_queue(
report_fn: Optional[Callable[[], None]],
q: Queue,
label: str,
*,
may_send: bool,
) -> None:
"""
Best-effort shutdown drain: optional timed flush when READY, then abandon + timed join.
Never blocks longer than flush+join budgets (prevents atexit hang when not READY).
"""
if may_send and report_fn is not None and not q.empty():
done = Event()
def _flush():
try:
report_fn()
except Exception: # noqa: BLE001 - shutdown must continue
logger.exception('shutdown flush failed for %s queue', label)
finally:
done.set()
Thread(target=_flush, name=f'sw-shutdown-flush-{label}', daemon=True).start()
if not done.wait(_SHUTDOWN_FLUSH_TIMEOUT_SEC):
logger.warning(
'shutdown flush timed out after %.1fs for %s queue; abandoning remainder',
_SHUTDOWN_FLUSH_TIMEOUT_SEC,
label,
)
abandoned = _abandon_sync_queue(q)
if abandoned:
log_dropped_throttled(label, abandoned, force=True)
if not _join_sync_queue(q, _SHUTDOWN_JOIN_TIMEOUT_SEC):
logger.warning(
'shutdown join timed out after %.1fs for %s queue (unfinished_tasks=%s)',
_SHUTDOWN_JOIN_TIMEOUT_SEC,
label,
getattr(q, 'unfinished_tasks', '?'),
)
async def _abandon_async_queue(q: asyncio.Queue) -> int:
abandoned = 0
while True:
try:
q.get_nowait()
q.task_done()
abandoned += 1
except asyncio.QueueEmpty:
break
return abandoned
async def _shutdown_async_queue(q: asyncio.Queue, label: str) -> None:
"""
Async reporters use unbounded queue.get() generators — do not await report() on
shutdown (can hang forever). Abandon + timed join, then caller cancels tasks.
"""
abandoned = await _abandon_async_queue(q)
if abandoned:
log_dropped_throttled(label, abandoned, force=True)
try:
await asyncio.wait_for(q.join(), timeout=_SHUTDOWN_JOIN_TIMEOUT_SEC)
except asyncio.TimeoutError:
logger.warning(
'shutdown join timed out after %.1fs for async %s queue',
_SHUTDOWN_JOIN_TIMEOUT_SEC,
label,
)
def _retrieve_background_task_outcome(
task: asyncio.Task,
*,
report_unexpected_completion: bool = True,
report_unexpected_cancellation: bool = False,
):
"""
Return a completed background task's exception when it should be reported.
Retrieves the exception so asyncio does not emit "never retrieved" warnings.
Successful completion / cancellation are only converted to errors when the
corresponding report_* flag is set (supervisor path before shutdown).
"""
if task is None or not task.done():
return None
if task.cancelled():
if report_unexpected_cancellation:
return RuntimeError(
'Python agent asyncio background task was cancelled unexpectedly'
)
return None
exc = task.exception()
if exc is not None:
return exc
if report_unexpected_completion:
return RuntimeError('Python agent asyncio background task finished unexpectedly')
return None
def _log_background_task_outcome(
task: asyncio.Task,
*,
report_unexpected_completion: bool = True,
report_unexpected_cancellation: bool = False,
) -> bool:
"""Log and retrieve a completed background task outcome. Returns True if logged."""
if getattr(task, '_sw_outcome_handled', False):
return False
exc = _retrieve_background_task_outcome(
task,
report_unexpected_completion=report_unexpected_completion,
report_unexpected_cancellation=report_unexpected_cancellation,
)
if exc is None:
return False
# Mark before logging so cleanup never re-logs a supervisor-handled outcome.
task._sw_outcome_handled = True
logger.error('Error in Python agent asyncio event loop: %s', exc, exc_info=exc)
return True
async def _await_shutdown_or_background_failure(
finished: asyncio.Event,
background_tasks,
) -> None:
"""
Wait for shutdown or the first unexpected background-task completion.
Background tasks are expected to run until shutdown. If one finishes or is
cancelled early, retrieve/log its outcome and signal shutdown so the root
can clean up.
"""
shutdown_waiter = asyncio.create_task(finished.wait())
pending = {task for task in background_tasks if task is not None}
try:
while not finished.is_set():
if not pending:
await finished.wait()
return
done, _ = await asyncio.wait(
pending | {shutdown_waiter},
return_when=asyncio.FIRST_COMPLETED,
)
if shutdown_waiter in done or finished.is_set():
return
for task in done:
pending.discard(task)
# Before shutdown, both early success and unexpected cancel are errors.
if _log_background_task_outcome(
task,
report_unexpected_completion=True,
report_unexpected_cancellation=True,
):
finished.set()
return
finally:
if not shutdown_waiter.done():
shutdown_waiter.cancel()
try:
await shutdown_waiter
except asyncio.CancelledError:
pass
async def _cancel_pending_tasks(tasks) -> None:
"""
Cancel agent-owned reporter / connectivity-watch tasks only.
Cancel only the given reporter / watch tasks; never the asyncio.run root.
The current task is excluded so we never await ourselves.
During cleanup, normal completion and intentional cancellation are silent;
only real exceptions that were not already handled by the supervisor are logged.
"""
current = asyncio.current_task()
for task in tasks:
if task is None or task is current:
continue
if task.done():
_log_background_task_outcome(
task,
report_unexpected_completion=False,
report_unexpected_cancellation=False,
)
pending = [
task for task in tasks
if task is not None and task is not current and not task.done()
]
if not pending:
return
for task in pending:
task.cancel()
try:
await asyncio.wait_for(
asyncio.gather(*pending, return_exceptions=True),
timeout=_SHUTDOWN_JOIN_TIMEOUT_SEC,
)
except asyncio.TimeoutError:
logger.warning(
'shutdown task cancellation timed out after %.1fs; %d task(s) still pending',
_SHUTDOWN_JOIN_TIMEOUT_SEC,
sum(1 for task in pending if not task.done()),
)
for task in pending:
_log_background_task_outcome(
task,
report_unexpected_completion=False,
report_unexpected_cancellation=False,
)
def _close_previous_protocol(protocol) -> None:
"""Close a replaced protocol channel (fork re-bootstrap / defensive re-open)."""
if protocol is None:
return
close = getattr(protocol, 'close', None)
if callable(close):
try:
close()
except Exception: # noqa: BLE001
logger.exception('failed to close previous protocol channel')
async def _aclose_previous_protocol(protocol) -> None:
"""Await aclose on the running loop. Never run_coroutine_threadsafe().result() here."""
if protocol is None:
return
aclose = getattr(protocol, 'aclose', None)
if callable(aclose):
try:
await aclose()
return
except Exception: # noqa: BLE001
logger.exception('failed to aclose previous protocol channel')
close = getattr(protocol, 'close', None)
if callable(close):
try:
close()
except Exception: # noqa: BLE001
logger.exception('failed to close previous protocol channel')
def report_with_backoff(reporter_name, init_wait):
"""
An exponential backoff for retrying reporters.
"""
def backoff_decorator(func):
@functools.wraps(func)
def backoff_wrapper(self, *args, **kwargs):
wait = base = init_wait
while not self._finished.is_set():
try:
flag = func(self, *args, **kwargs)
# for segment/log reporter, if the queue not empty(return True), we should keep reporter working
# for other cases(return false or None), reset to base wait time on success
wait = 0 if flag else base
except Exception: # noqa
wait = min(60, wait * 2 or 1) # double wait time with each consecutive error up to a maximum
log_reporter_exception_throttled(reporter_name, wait)
self._finished.wait(wait)
logger.info('finished reporter thread')
return backoff_wrapper
return backoff_decorator
def report_with_backoff_async(reporter_name, init_wait):
"""
An exponential async backoff for retrying reporters.
"""
def backoff_decorator(func):
@functools.wraps(func)
async def backoff_wrapper(self, *args, **kwargs):
wait = base = init_wait
while not self._finished.is_set():
try:
flag = await func(self, *args, **kwargs)
# for segment/log reporter, if the queue not empty(return True), we should keep reporter working
# for other cases(return false or None), reset to base wait time on success
wait = 0 if flag else base
except Exception: # noqa
wait = min(60, wait * 2 or 1) # double wait time with each consecutive error up to a maximum
log_reporter_exception_throttled(reporter_name, wait)
# Prefer Event.wait so shutdown (_finished.set) wakes immediately;
# plain sleep(wait) would otherwise block up to heartbeat/backoff period.
if wait:
try:
await asyncio.wait_for(self._finished.wait(), timeout=wait)
except asyncio.TimeoutError:
pass
else:
await asyncio.sleep(0)
logger.info('finished reporter coroutine')
return backoff_wrapper
return backoff_decorator
class SkyWalkingAgent(Singleton):
"""
The main singleton class and entrypoint of SkyWalking Python Agent.
Upon fork(), original instance rebuild everything (queues, threads, instrumentation) by
calling the fork handlers in the class instance.
"""
__started: bool = False # shared by all instances
def __init__(self):
"""
Protocol is one of gRPC, HTTP and Kafka that
provides clients to reporters to communicate with OAP backend.
"""
self.started_pid = None
self.__protocol: Optional[Protocol] = None
self._finished: Optional[Event] = None
# True only after __bootstrap() in the current process; stays False in a pre-fork master
self.__reporting: bool = False
self.__at_fork_registered: bool = False
self.__fini_registered: bool = False
def __bootstrap(self):
# when forking, already instrumented modules must not be instrumented again
# otherwise it will cause double instrumentation! (we should provide an un-instrument method)
# Initialize queues for segment, log, meter and profiling snapshots
self.__init_queues()
if config.agent_protocol == 'grpc':
from skywalking.agent.protocol.grpc import GrpcProtocol
_close_previous_protocol(self.__protocol)
self.__protocol = GrpcProtocol()
elif config.agent_protocol == 'http':
from skywalking.agent.protocol.http import HttpProtocol
_close_previous_protocol(self.__protocol)
self.__protocol = HttpProtocol()
elif config.agent_protocol == 'kafka':
from skywalking.agent.protocol.kafka import KafkaProtocol
_close_previous_protocol(self.__protocol)
self.__protocol = KafkaProtocol()
# Start reporter threads and register queues
self.__init_threading()
self.__reporting = True
def __init_queues(self) -> None:
"""
This method initializes all the queues for the agent and reporters.
"""
self.__segment_queue = Queue(maxsize=config.agent_trace_reporter_max_buffer_size)
self.__log_queue: Optional[Queue] = None
self.__meter_queue: Optional[Queue] = None
self.__snapshot_queue: Optional[Queue] = None
if config.agent_meter_reporter_active:
self.__meter_queue = Queue(maxsize=config.agent_meter_reporter_max_buffer_size)
if config.agent_log_reporter_active:
self.__log_queue = Queue(maxsize=config.agent_log_reporter_max_buffer_size)
if config.agent_profile_active:
self.__snapshot_queue = Queue(maxsize=config.agent_profile_snapshot_transport_buffer_size)
def __init_threading(self) -> None:
"""
This method initializes all the threads for the agent and reporters.
Upon os.fork(), callback will reinitialize threads and queues by calling this method
Heartbeat thread is started by default.
Segment reporter thread and segment queue is created by default.
All other queues and threads depends on user configuration.
"""
self._finished = Event()
__heartbeat_thread = Thread(name='HeartbeatThread', target=self.__heartbeat, daemon=True)
__heartbeat_thread.start()
__segment_report_thread = Thread(name='SegmentReportThread', target=self.__report_segment, daemon=True)
__segment_report_thread.start()
if config.agent_meter_reporter_active:
__meter_report_thread = Thread(name='MeterReportThread', target=self.__report_meter, daemon=True)
__meter_report_thread.start()
if config.agent_pvm_meter_reporter_active:
from skywalking.meter.pvm.cpu_usage import CPUUsageDataSource
from skywalking.meter.pvm.gc_data import GCDataSource
from skywalking.meter.pvm.mem_usage import MEMUsageDataSource
from skywalking.meter.pvm.thread_data import ThreadDataSource
MEMUsageDataSource().register()
CPUUsageDataSource().register()
GCDataSource().register()
ThreadDataSource().register()
if config.agent_log_reporter_active:
__log_report_thread = Thread(name='LogReportThread', target=self.__report_log, daemon=True)
__log_report_thread.start()
if config.agent_profile_active:
# Now only profiler receives commands from OAP
__command_dispatch_thread = Thread(name='CommandDispatchThread', target=self.__command_dispatch,
daemon=True)
__command_dispatch_thread.start()
__query_profile_thread = Thread(name='QueryProfileCommandThread', target=self.__query_profile_command,
daemon=True)
__query_profile_thread.start()
__send_profile_thread = Thread(name='SendProfileSnapShotThread', target=self.__send_profile_snapshot,
daemon=True)
__send_profile_thread.start()
@staticmethod # for now
def __fork_before() -> None:
"""
This handles explicit fork() calls. The child process will not have a running thread, so we need to
revive all of them. The parent process will continue to run as normal.
This does not affect pre-forking server support, which are handled separately.
"""
# possible deadlock would be introduced if some queue is in use when fork() is called and
# therefore child process will inherit a locked queue. To avoid this and have side benefit
# of a clean queue in child process (prevent duplicated reporting), we simply restart the agent and
# reinitialize all queues and threads.
logger.warning('SkyWalking Python agent fork support is currently experimental, '
'please report issues if you encounter any.')
@staticmethod # for now
def __fork_after_in_parent() -> None:
"""
Something to do after fork() in parent process
"""
...
def __fork_after_in_child(self) -> None:
"""
Simply restart the agent after we detect a fork() call
"""
# This will be used by os.fork() called by application and also Gunicorn, not uWSGI
# otherwise we assume a fork() happened, give it a new service instance name
logger.info('New process detected, re-initializing SkyWalking Python agent')
# Note: this is for experimental change, default config should never reach here
# Fork support is controlled by config.agent_fork_support :default: False
# Important: This does not impact pre-forking server support (uwsgi, gunicorn, etc...)
# This is only for explicit long-running fork() calls.
config.agent_instance_name = f'{config.agent_instance_name}-child({os.getpid()})'
self.start()
logger.info(f'Agent spawned as {config.agent_instance_name} for service {config.agent_name}.')
def start(self) -> None:
"""
Start would be called by user or os.register_at_fork() callback
Start will proceed if and only if the agent is not started in the
current process.
When os.fork(), the service instance should be changed to a new one by appending pid.
"""
loggings.init()
if sys.version_info < (3, 7):
# agent may or may not work for Python 3.6 and below
# since 3.6 is EOL, we will not officially support it
logger.warning('SkyWalking Python agent does not support Python 3.6 and below, '
'please upgrade to Python 3.7 or above.')
# Required for grpcio to work with fork(), see grpc/grpc doc/fork_support.md.
if config.agent_protocol == 'grpc' and config.agent_experimental_fork_support:
python_major_version: tuple = sys.version_info[:2]
if python_major_version == (3, 7):
logger.warning('gRPC fork support may cause hanging on Python 3.7 '
'when used together with gRPC and subprocess lib'
'See: https://github.com/grpc/grpc/issues/18075.'
'Please consider upgrade to Python 3.8+, '
'or use HTTP/Kafka protocol, or disable experimental fork support '
'if your application did not start successfully.')
# GRPC_POLL_STRATEGY=poll must NOT be set: the legacy poller is broken across
# fork() on grpcio >= 1.80 (the agent requires >= 1.83),
# see https://github.com/apache/skywalking/issues/13958
os.environ['GRPC_ENABLE_FORK_SUPPORT'] = 'true' # must precede `import grpc`
if not os.getenv('prefork'): # Gunicorn prefork creates channels only after fork() and is safe
logger.warning('Explicit os.fork() with a live gRPC channel is unreliable on '
'grpcio >= 1.80 (see grpc/grpc#43055) and may silently break '
'reporting in either process; prefer SW_AGENT_PROTOCOL=http or '
'kafka for forking applications.')
if not self.__started:
# if not already started, start the agent
logger.info(f'SkyWalking sync agent instance {config.agent_instance_name} starting in pid-{os.getpid()}.')
self.__init_instrumentation()
elif self.__started and os.getpid() == self.started_pid:
# if already started, and this is the same process, raise an error
raise RuntimeError('SkyWalking Python agent has already been started in this process, '
'did you call start more than once in your code + sw-python CLI? '
'If you already use sw-python CLI, you should remove the manual start(), vice versa.')
# Else there's a new process (after fork()), we will restart the agent in the new process
self.started_pid = os.getpid()
flag = False
try:
from gevent import monkey
flag = monkey.is_module_patched('socket')
except ModuleNotFoundError:
logger.debug("it was found that no gevent was used, if you don't use, please ignore.")
if flag:
import grpc.experimental.gevent as grpc_gevent
grpc_gevent.init_gevent()
if config.agent_profile_active:
profile.init()
if config.agent_meter_reporter_active:
meter.init(force=True) # force re-init after fork()
if config.sample_n_per_3_secs > 0:
sampling.init(force=True)
self.__bootstrap() # calls init_threading
# atexit registrations are fork-inherited; register once per lineage so a
# fork-restarted child does not stack a duplicate __fini
if not self.__fini_registered:
self.__fini_registered = True
atexit.register(self.__fini)
if config.agent_experimental_fork_support:
self.__register_fork_hooks()
def __init_instrumentation(self) -> None:
"""
Install instrumentation once per process lineage; forked children inherit
the patches and the __started flag, so they skip this.
"""
config.finalize() # Must be finalized exactly once
self.__started = True
# Install log reporter core
if config.agent_log_reporter_active:
from skywalking import log
log.install()
# Here we install all other lib plugins on first time start (parent process)
plugins.install()
def __register_fork_hooks(self) -> None:
# at-fork registrations cannot be removed and are fork-inherited; never register twice
if self.__at_fork_registered:
return
if hasattr(os, 'register_at_fork'):
self.__at_fork_registered = True
os.register_at_fork(before=self.__fork_before, after_in_parent=self.__fork_after_in_parent,
after_in_child=self.__fork_after_in_child)
def start_prefork_master(self) -> None:
"""
Prepare the agent in a pre-forking server master (Gunicorn): install instrumentation
and arm the fork hooks only. The full agent — queues, gRPC channel, reporter threads —
starts in each forked worker; a channel living across fork() is unsafe with
grpcio >= 1.80, see https://github.com/apache/skywalking/issues/13958.
"""
loggings.init()
if config.agent_protocol == 'grpc' and config.agent_experimental_fork_support:
# Must be exported before the first `import grpc`; plugins.install() below imports grpc
os.environ['GRPC_ENABLE_FORK_SUPPORT'] = 'true'
if not self.__started:
self.__init_instrumentation()
logger.info(f'SkyWalking Python agent instrumented pre-fork master pid-{os.getpid()}, '
f'reporters will start in forked worker processes.')
self.started_pid = os.getpid()
if config.agent_experimental_fork_support:
self.__register_fork_hooks()
def __fini(self):
"""
This method is called when the agent is shutting down.
Clean up all the queues and threads.
Stop reporter loops first, then best-effort flush (timed) when READY, else
abandon queued items so Queue.join() cannot hang forever (READY-gate gap).
"""
if not self.__reporting: # never bootstrapped in this process (e.g. pre-fork master)
return
# Wake backoff loops immediately; do not leave finished.set() until after joins.
self._finished.set()
may_send = self.__protocol.is_ready()
_shutdown_sync_queue(
(lambda: self.__protocol.report_segment(self.__segment_queue, False)) if may_send else None,
self.__segment_queue,
'segment',
may_send=may_send,
)
if config.agent_log_reporter_active:
_shutdown_sync_queue(
(lambda: self.__protocol.report_log(self.__log_queue, False)) if may_send else None,
self.__log_queue,
'log',
may_send=may_send,
)
if config.agent_profile_active:
_shutdown_sync_queue(
(lambda: self.__protocol.report_snapshot(self.__snapshot_queue, False)) if may_send else None,
self.__snapshot_queue,
'snapshot',
may_send=may_send,
)
if config.agent_meter_reporter_active:
_shutdown_sync_queue(
(lambda: self.__protocol.report_meter(self.__meter_queue, False)) if may_send else None,
self.__meter_queue,
'meter',
may_send=may_send,
)
close = getattr(self.__protocol, 'close', None)
if callable(close):
close()
def stop(self) -> None:
"""
Stops the agent and reset the started flag.
"""
atexit.unregister(self.__fini)
self.__fini_registered = False
self.__fini()
self.__reporting = False
self.__started = False
@report_with_backoff(reporter_name='heartbeat', init_wait=config.agent_collector_heartbeat_period)
def __heartbeat(self) -> bool:
# Until subscribe reports READY, retry soon (do not wait a full heartbeat period).
if not self.__protocol.is_ready():
time.sleep(0.5)
return True
self.__protocol.heartbeat()
return False
# segment/log init_wait is set to 0.02 to prevent threads from hogging the cpu too much
# The value of 0.02(20 ms) is set to be consistent with the queue delay of the Java agent
@report_with_backoff(reporter_name='segment', init_wait=0.02)
def __report_segment(self) -> bool:
"""Returns True if the queue is not empty and a send was attempted."""
# Not READY: sleep then wait=0 — avoid 20ms busy-loop + IDLE nudge thrash.
if not self.__protocol.is_ready():
time.sleep(0.5)
return True
queue_not_empty_flag = not self.__segment_queue.empty()
if queue_not_empty_flag:
self.__protocol.report_segment(self.__segment_queue)
return queue_not_empty_flag
@report_with_backoff(reporter_name='log', init_wait=0.02)
def __report_log(self) -> bool:
"""Returns True if the queue is not empty and a send was attempted."""
if not self.__protocol.is_ready():
time.sleep(0.5)
return True
queue_not_empty_flag = not self.__log_queue.empty()
if queue_not_empty_flag:
self.__protocol.report_log(self.__log_queue)
return queue_not_empty_flag
@report_with_backoff(reporter_name='meter', init_wait=config.agent_meter_reporter_period)
def __report_meter(self) -> None:
if not self.__protocol.is_ready():
time.sleep(0.5)
return
if not self.__meter_queue.empty():
self.__protocol.report_meter(self.__meter_queue)
@report_with_backoff(reporter_name='profile_snapshot', init_wait=0.5)
def __send_profile_snapshot(self) -> None:
if not self.__protocol.is_ready():
time.sleep(0.5)
return
if not self.__snapshot_queue.empty():
self.__protocol.report_snapshot(self.__snapshot_queue)
@report_with_backoff(reporter_name='query_profile_command',
init_wait=config.agent_collector_get_profile_task_interval)
def __query_profile_command(self) -> None:
self.__protocol.query_profile_commands()
@staticmethod
def __command_dispatch() -> None:
# command dispatch will stuck when there are no commands
command_service.dispatch()
def started(self) -> bool:
"""
Whether reporting (queues, protocol clients, reporter threads) is active in this process.
False in a pre-forking server master, where only instrumentation is installed.
"""
return self.__reporting
def is_segment_queue_full(self):
if not self.__reporting:
return True # treated as full so span creation short-circuits to NoopSpan
return self.__segment_queue.full()
def archive_segment(self, segment: 'Segment'):
if not self.__reporting:
return
try: # unlike checking __queue.full() then inserting, this is atomic
self.__segment_queue.put(segment, block=False)
except Full:
log_dropped_throttled('segment')
def archive_log(self, log_data: 'LogData'):
if not self.__reporting:
return
try:
self.__log_queue.put(log_data, block=False)
except Full:
log_dropped_throttled('log')
def archive_meter(self, meter_data: 'MeterData'):
if not self.__reporting:
return
try:
self.__meter_queue.put(meter_data, block=False)
except Full:
log_dropped_throttled('meter')
def add_profiling_snapshot(self, snapshot: TracingThreadSnapshot):
if not self.__reporting:
return
try:
self.__snapshot_queue.put_nowait(snapshot)
except Full:
log_dropped_throttled('snapshot')
def notify_profile_finish(self, task: ProfileTask):
try:
self.__protocol.notify_profile_task_finish(task)
except Exception as e:
logger.error(f'notify profile task finish to backend fail. {str(e)}')
class SkyWalkingAgentAsync(Singleton):
__started: bool = False # shared by all instances
def __init__(self):
"""
ProtocolAsync is one of gRPC, HTTP and Kafka that
provides async clients to reporters to communicate with OAP backend.
"""
self.started_pid = None
self.__protocol: Optional[ProtocolAsync] = None
self._finished: Optional[asyncio.Event] = None
# Initialize asyncio queues for segment, log, meter and profiling snapshots
self.__segment_queue: Optional[asyncio.Queue] = None
self.__log_queue: Optional[asyncio.Queue] = None
self.__meter_queue: Optional[asyncio.Queue] = None
self.__snapshot_queue: Optional[asyncio.Queue] = None
self.event_loop_thread: Optional[Thread] = None
async def __bootstrap(self):
await _aclose_previous_protocol(self.__protocol)
if config.agent_protocol == 'grpc':
from skywalking.agent.protocol.grpc_aio import GrpcProtocolAsync
self.__protocol = GrpcProtocolAsync()
elif config.agent_protocol == 'http':
from skywalking.agent.protocol.http_aio import HttpProtocolAsync
self.__protocol = HttpProtocolAsync()
elif config.agent_protocol == 'kafka':
from skywalking.agent.protocol.kafka_aio import KafkaProtocolAsync
self.__protocol = KafkaProtocolAsync()
else:
raise ValueError(f'Unsupported protocol: {config.agent_protocol}')
logger.info(f'You are using {config.agent_protocol} protocol to communicate with OAP backend')
# Start reporter's asyncio coroutines and register queues
self.__init_coroutine()
def __init_coroutine(self) -> None:
"""
This method initializes all asyncio coroutines for the agent and reporters.
Heartbeat task is started by default.
Segment reporter task and segment queue is created by default.
All other queues and tasks depends on user configuration.
"""
self.background_coroutines = set()
watch = getattr(self.__protocol, 'watch_connectivity', None)
if callable(watch):
self.background_coroutines.add(watch())
self.background_coroutines.add(self.__heartbeat())
self.background_coroutines.add(self.__report_segment())
if config.agent_meter_reporter_active:
self.background_coroutines.add(self.__report_meter())
if config.agent_pvm_meter_reporter_active:
from skywalking.meter.pvm.cpu_usage import CPUUsageDataSource
from skywalking.meter.pvm.gc_data import GCDataSource
from skywalking.meter.pvm.mem_usage import MEMUsageDataSource
from skywalking.meter.pvm.thread_data import ThreadDataSource
MEMUsageDataSource().register()
CPUUsageDataSource().register()
GCDataSource().register()
ThreadDataSource().register()
if config.agent_log_reporter_active:
self.background_coroutines.add(self.__report_log())
if config.agent_profile_active:
self.background_coroutines.add(self.__command_dispatch())
self.background_coroutines.add(self.__query_profile_command())
self.background_coroutines.add(self.__send_profile_snapshot())
async def __start_event_loop_async(self) -> None:
self.loop = asyncio.get_running_loop() # always get the current running loop first
# asyncio Queue should be created after the creation of event loop
self.__segment_queue = asyncio.Queue(maxsize=config.agent_trace_reporter_max_buffer_size)
if config.agent_meter_reporter_active:
self.__meter_queue = asyncio.Queue(maxsize=config.agent_meter_reporter_max_buffer_size)
if config.agent_log_reporter_active:
self.__log_queue = asyncio.Queue(maxsize=config.agent_log_reporter_max_buffer_size)
if config.agent_profile_active:
self.__snapshot_queue = asyncio.Queue(maxsize=config.agent_profile_snapshot_transport_buffer_size)
# initialize background coroutines
self.background_coroutines = set()
self.background_tasks = set()
self._finished = asyncio.Event()
# Install log reporter core
if config.agent_log_reporter_active:
from skywalking import log
log.install()
if config.agent_meter_reporter_active:
# meter.init(force=True)
await meter.init_async()
if config.sample_n_per_3_secs > 0:
await sampling.init_async()
await self.__bootstrap() # gather all coroutines
self.background_tasks = {asyncio.create_task(coro) for coro in self.background_coroutines}
logger.debug('All background coroutines started')
# Wait for shutdown or unexpected background-task completion inside the
# asyncio.run root, then clean up here so the Runner stays alive through
# protocol aclose() before asyncio.run returns.
await _await_shutdown_or_background_failure(self._finished, self.background_tasks)
await self.__async_shutdown_cleanup()
def __start_event_loop(self) -> None:
try:
asyncio.run(self.__start_event_loop_async())
except asyncio.CancelledError:
logger.info('Python agent asyncio event loop is closed')
except Exception as e:
logger.error(f'Error in Python agent asyncio event loop: {e}')
finally:
if self._finished is not None:
self._finished.set()
def start(self) -> None:
loggings.init()
if sys.version_info < (3, 7):
# agent may or may not work for Python 3.6 and below
# since 3.6 is EOL, we will not officially support it
logger.warning('SkyWalking Python agent does not support Python 3.6 and below, '
'please upgrade to Python 3.7 or above.')
if not self.__started:
# if not already started, start the agent
config.finalize() # Must be finalized exactly once
self.__started = True
logger.info(f'SkyWalking async agent instance {config.agent_instance_name} starting in pid-{os.getpid()}.')
# Here we install all other lib plugins on first time start (parent process)
plugins.install()
elif self.__started and os.getpid() == self.started_pid:
# if already started, and this is the same process, raise an error
raise RuntimeError('SkyWalking Python agent has already been started in this process, '
'did you call start more than once in your code + sw-python CLI? '
'If you already use sw-python CLI, you should remove the manual start(), vice versa.')
self.started_pid = os.getpid()
atexit.register(self.__fini)
# still init profile here, since it is using threading rather than asyncio
if config.agent_profile_active:
profile.init()
self.event_loop_thread = Thread(name='event_loop_thread', target=self.__start_event_loop, daemon=True)
self.event_loop_thread.start()
async def __async_shutdown_cleanup(self) -> None:
"""
Async shutdown body that must run on the asyncio.run root task.
Do not await report_* here: aio generators use unbounded queue.get() and can
hang forever. Abandon + timed join, then cancel tasks.
"""
await _shutdown_async_queue(self.__segment_queue, 'segment')
if config.agent_log_reporter_active:
await _shutdown_async_queue(self.__log_queue, 'log')
if config.agent_profile_active:
await _shutdown_async_queue(self.__snapshot_queue, 'snapshot')
if config.agent_meter_reporter_active:
await _shutdown_async_queue(self.__meter_queue, 'meter')
await _cancel_pending_tasks(getattr(self, 'background_tasks', ()))
aclose = getattr(self.__protocol, 'aclose', None)
if callable(aclose):
try:
await aclose()
except Exception: # noqa: BLE001
pass
else:
close = getattr(self.__protocol, 'close', None)
if callable(close):
close()
def __fini(self):
loop = getattr(self, 'loop', None)
if loop is not None and not loop.is_closed() and self._finished is not None:
loop.call_soon_threadsafe(self._finished.set)
if self.event_loop_thread is not None:
self.event_loop_thread.join(
timeout=_SHUTDOWN_LOOP_JOIN_TIMEOUT_SEC + _SHUTDOWN_JOIN_TIMEOUT_SEC * 4,
)
if self.event_loop_thread.is_alive():
logger.warning(
'Python agent event_loop thread still alive after %.1fs shutdown budget',
_SHUTDOWN_LOOP_JOIN_TIMEOUT_SEC,
)
else:
logger.info('Finished Python agent event_loop thread')
# TODO: Unhandled error in sys.excepthook https://github.com/pytest-dev/execnet/issues/30
def stop(self) -> None:
"""
Stops the agent and reset the started flag.
"""
atexit.unregister(self.__fini)
self.__fini()
self.__started = False
@report_with_backoff_async(reporter_name='heartbeat', init_wait=config.agent_collector_heartbeat_period)
async def __heartbeat(self) -> bool:
if not self.__protocol.is_ready():
await asyncio.sleep(0.5)
return True
await self.__protocol.heartbeat()
return False
@report_with_backoff_async(reporter_name='segment', init_wait=0.02)
async def __report_segment(self) -> bool:
"""Returns True if the queue is not empty and a send was attempted."""
if not self.__protocol.is_ready():
await asyncio.sleep(0.5)
return True
queue_not_empty_flag = not self.__segment_queue.empty()
if queue_not_empty_flag:
await self.__protocol.report_segment(self.__segment_queue)
return queue_not_empty_flag
@report_with_backoff_async(reporter_name='log', init_wait=0.02)
async def __report_log(self) -> bool:
"""Returns True if the queue is not empty and a send was attempted."""
if not self.__protocol.is_ready():
await asyncio.sleep(0.5)
return True
queue_not_empty_flag = not self.__log_queue.empty()
if queue_not_empty_flag:
await self.__protocol.report_log(self.__log_queue)
return queue_not_empty_flag
@report_with_backoff_async(reporter_name='meter', init_wait=config.agent_meter_reporter_period)
async def __report_meter(self) -> None:
if not self.__protocol.is_ready():
await asyncio.sleep(0.5)
return
if not self.__meter_queue.empty():
await self.__protocol.report_meter(self.__meter_queue)
@report_with_backoff_async(reporter_name='profile_snapshot', init_wait=0.5)
async def __send_profile_snapshot(self) -> None:
if not self.__protocol.is_ready():
await asyncio.sleep(0.5)
return
if not self.__snapshot_queue.empty():
await self.__protocol.report_snapshot(self.__snapshot_queue)
@report_with_backoff_async(
reporter_name='query_profile_command',
init_wait=config.agent_collector_get_profile_task_interval)
async def __query_profile_command(self) -> None:
await self.__protocol.query_profile_commands()
@staticmethod
async def __command_dispatch() -> None:
# command dispatch will stuck when there are no commands
await command_service_async.dispatch()
def __asyncio_queue_put_nowait(self, q: asyncio.Queue, queue_name: str, item):
try:
q.put_nowait(item)
except asyncio.QueueFull:
log_dropped_throttled(queue_name)
def is_segment_queue_full(self):
return self.__segment_queue.full()
def archive_segment(self, segment: 'Segment'):
if not self.loop.is_closed():
self.loop.call_soon_threadsafe(self.__asyncio_queue_put_nowait, self.__segment_queue, 'segment', segment)
def archive_log(self, log_data: 'LogData'):
if not self.loop.is_closed():
self.loop.call_soon_threadsafe(self.__asyncio_queue_put_nowait, self.__log_queue, 'log', log_data)
def archive_meter(self, meter_data: 'MeterData'):
if not self.loop.is_closed():
self.loop.call_soon_threadsafe(self.__asyncio_queue_put_nowait, self.__meter_queue, 'meter', meter_data)
async def archive_meter_async(self, meter_data: 'MeterData'):
try:
self.__meter_queue.put_nowait(meter_data)
except asyncio.QueueFull:
log_dropped_throttled('meter')
def add_profiling_snapshot(self, snapshot: TracingThreadSnapshot):
self.loop.call_soon_threadsafe(self.__asyncio_queue_put_nowait, self.__snapshot_queue, 'snapshot', snapshot)
def notify_profile_finish(self, task: ProfileTask):
try:
asyncio.run_coroutine_threadsafe(self.__protocol.notify_profile_task_finish(task), self.loop)
except Exception as e:
logger.error(f'notify profile task finish to backend fail. {e}')
# Export for user (backwards compatibility)
# so users still use `from skywalking import agent`
agent = SkyWalkingAgentAsync() if config.agent_asyncio_enhancement else SkyWalkingAgent()
start = agent.start