Skip to content

Commit 35da9da

Browse files
committed
SERVER-38045 Add GDB tools for dumping the SessionCatalog in the hang analyzer
1 parent cee9c4d commit 35da9da

File tree

3 files changed

+267
-1
lines changed

3 files changed

+267
-1
lines changed

buildscripts/gdb/mongo.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@
77

88
import gdb
99

10+
# pylint: disable=invalid-name,wildcard-import
11+
try:
12+
# Try to find and load the C++ pretty-printer library.
13+
import glob
14+
pp = glob.glob("/opt/mongodbtoolchain/v2/share/gcc-*/python/libstdcxx/v6/printers.py")
15+
printers = pp[0]
16+
path = os.path.dirname(os.path.dirname(os.path.dirname(printers)))
17+
sys.path.insert(0, path)
18+
from libstdcxx.v6.printers import *
19+
print("Loaded libstdc++ pretty printers from '%s'" % printers)
20+
except ImportError as e:
21+
print("Failed to load the libstdc++ pretty printers: " + str(e))
22+
# pylint: enable=invalid-name,wildcard-import
23+
24+
if sys.version_info[0] >= 3:
25+
# GDB only permits converting a gdb.Value instance to its numerical address when using the
26+
# long() constructor in Python 2 and not when using the int() constructor. We define the
27+
# 'long' class as an alias for the 'int' class in Python 3 for compatibility.
28+
long = int # pylint: disable=redefined-builtin,invalid-name
29+
1030

1131
def get_process_name():
1232
"""Return the main binary we are attached to."""
@@ -49,6 +69,88 @@ def get_current_thread_name():
4969
return fallback_name
5070

5171

72+
def get_global_service_context():
73+
"""Return the global ServiceContext object."""
74+
return gdb.parse_and_eval("'mongo::(anonymous namespace)::globalServiceContext'").dereference()
75+
76+
77+
def get_session_catalog():
78+
"""Return the global SessionCatalog object.
79+
80+
Returns None if no SessionCatalog could be found.
81+
"""
82+
# The SessionCatalog is a decoration on the ServiceContext.
83+
session_catalog_dec = get_decoration(get_global_service_context(), "mongo::SessionCatalog")
84+
if session_catalog_dec is None:
85+
return None
86+
return session_catalog_dec[1]
87+
88+
89+
def get_decorations(obj):
90+
"""Return an iterator to all decorations on a given object.
91+
92+
Each object returned by the iterator is a tuple whose first element is the type name of the
93+
decoration and whose second element is the decoration object itself.
94+
95+
TODO: De-duplicate the logic between here and DecorablePrinter. This code was copied from there.
96+
"""
97+
type_name = str(obj.type).replace(" ", "")
98+
decorable = obj.cast(gdb.lookup_type("mongo::Decorable<{}>".format(type_name)))
99+
decl_vector = decorable["_decorations"]["_registry"]["_decorationInfo"]
100+
start = decl_vector["_M_impl"]["_M_start"]
101+
finish = decl_vector["_M_impl"]["_M_finish"]
102+
103+
decorable_t = decorable.type.template_argument(0)
104+
decinfo_t = gdb.lookup_type('mongo::DecorationRegistry<{}>::DecorationInfo'.format(decorable_t))
105+
count = long((long(finish) - long(start)) / decinfo_t.sizeof)
106+
107+
for i in range(count):
108+
descriptor = start[i]
109+
dindex = int(descriptor["descriptor"]["_index"])
110+
111+
type_name = str(descriptor["constructor"])
112+
type_name = type_name[0:len(type_name) - 1]
113+
type_name = type_name[0:type_name.rindex(">")]
114+
type_name = type_name[type_name.index("constructAt<"):].replace("constructAt<", "")
115+
# get_unique_ptr should be loaded from 'mongo_printers.py'.
116+
decoration_data = get_unique_ptr(decorable["_decorations"]["_decorationData"]) # pylint: disable=undefined-variable
117+
118+
if type_name.endswith('*'):
119+
type_name = type_name[0:len(type_name) - 1]
120+
type_name = type_name.rstrip()
121+
type_t = gdb.lookup_type(type_name)
122+
obj = decoration_data[dindex].cast(type_t)
123+
yield (type_name, obj)
124+
125+
126+
def get_decoration(obj, type_name):
127+
"""Find a decoration on 'obj' where the string 'type_name' is in the decoration's type name.
128+
129+
Returns a tuple whose first element is the type name of the decoration and whose
130+
second is the decoration itself. If there are multiple such decorations, returns the first one
131+
that matches. Returns None if no matching decorations were found.
132+
"""
133+
for dec_type_name, dec in get_decorations(obj):
134+
if type_name in dec_type_name:
135+
return (dec_type_name, dec)
136+
return None
137+
138+
139+
def get_boost_optional(optional):
140+
"""
141+
Retrieve the value stored in a boost::optional type, if it is non-empty.
142+
143+
Returns None if the optional is empty.
144+
145+
TODO: Import the boost pretty printers instead of using this custom function.
146+
"""
147+
if not optional['m_initialized']:
148+
return None
149+
value_ref_type = optional.type.template_argument(0).pointer()
150+
storage = optional['m_storage']['dummy_']['data']
151+
return storage.cast(value_ref_type).dereference()
152+
153+
52154
###################################################################################################
53155
#
54156
# Commands
@@ -91,6 +193,140 @@ def invoke(self, arg, _from_tty): # pylint: disable=no-self-use,unused-argument
91193
DumpGlobalServiceContext()
92194

93195

196+
class GetMongoDecoration(gdb.Command):
197+
"""
198+
Search for a decoration on an object by typename and print it e.g.
199+
200+
(gdb) mongo-decoration opCtx ReadConcernArgs
201+
202+
would print out a decoration on opCtx whose type name contains the string "ReadConcernArgs".
203+
"""
204+
205+
def __init__(self):
206+
"""Initialize GetMongoDecoration."""
207+
RegisterMongoCommand.register(self, "mongo-decoration", gdb.COMMAND_DATA)
208+
209+
def invoke(self, args, _from_tty): # pylint: disable=unused-argument,no-self-use
210+
"""Invoke GetMongoDecoration."""
211+
argarr = args.split(" ")
212+
if len(argarr) < 2:
213+
raise ValueError("Must provide both an object and type_name argument.")
214+
215+
# The object that is decorated.
216+
expr = argarr[0]
217+
# The substring of the decoration type that is to be printed.
218+
type_name_substr = argarr[1]
219+
dec = get_decoration(gdb.parse_and_eval(expr), type_name_substr)
220+
if dec:
221+
(type_name, obj) = dec
222+
print(type_name, obj)
223+
else:
224+
print("No decoration found whose type name contains '" + type_name_substr + "'.")
225+
226+
227+
# Register command
228+
GetMongoDecoration()
229+
230+
231+
class DumpMongoDSessionCatalog(gdb.Command):
232+
"""Print out the mongod SessionCatalog, which maintains a table of all Sessions.
233+
234+
Prints out interesting information from TransactionParticipants too, which are decorations on
235+
the Session. If no arguments are provided, dumps out all sessions. Can optionally provide a
236+
session id argument. In that case, will only print the session for the specified id, if it is
237+
found. e.g.
238+
239+
(gdb) dump-sessions "32cb9e84-98ad-4322-acf0-e055cad3ef73"
240+
241+
"""
242+
243+
def __init__(self):
244+
"""Initialize DumpMongoDSessionCatalog."""
245+
RegisterMongoCommand.register(self, "mongod-dump-sessions", gdb.COMMAND_DATA)
246+
247+
def invoke(self, args, _from_tty): # pylint: disable=unused-argument,no-self-use,too-many-locals
248+
"""Invoke DumpMongoDSessionCatalog."""
249+
# See if a particular session id was specified.
250+
argarr = args.split(" ")
251+
lsid_to_find = None
252+
if argarr:
253+
lsid_to_find = argarr[0]
254+
255+
# Get the SessionCatalog and the table of sessions.
256+
session_catalog = get_session_catalog()
257+
if session_catalog is None:
258+
print(
259+
"No SessionCatalog object was found on the ServiceContext. Not dumping any sessions."
260+
)
261+
return
262+
lsid_map = session_catalog["_sessions"]
263+
session_kv_pairs = list(StdHashtableIterator(lsid_map['_M_h'])) # pylint: disable=undefined-variable
264+
print("Dumping %d Session objects from the SessionCatalog" % len(session_kv_pairs))
265+
266+
# Optionally search for a specified session, based on its id.
267+
if lsid_to_find:
268+
print("Only printing information for session " + lsid_to_find + ", if found.")
269+
lsids_to_print = [lsid_to_find]
270+
else:
271+
lsids_to_print = [str(s['first']['_id']) for s in session_kv_pairs]
272+
273+
for sess_kv in session_kv_pairs:
274+
# The Session is stored inside the SessionRuntimeInfo object.
275+
session_runtime_info = sess_kv['second']['_M_ptr'].dereference()
276+
session = session_runtime_info['session']
277+
# TODO: Add a custom pretty printer for LogicalSessionId.
278+
lsid_str = str(session['_sessionId']['_id'])
279+
280+
# If we are only interested in a specific session, then we print out the entire Session
281+
# object, to aid more detailed debugging.
282+
if lsid_str == lsid_to_find:
283+
print("SessionId", "=", lsid_str)
284+
print(session)
285+
# Terminate if this is the only session we care about.
286+
break
287+
288+
# Only print info for the necessary sessions.
289+
if lsid_str not in lsids_to_print:
290+
continue
291+
292+
# If we are printing multiple sessions, we only print the most interesting fields from
293+
# the Session object for the sake of efficiency. We print the session id string first so
294+
# the session is easily identifiable.
295+
print("Session (" + str(session.address) + "):")
296+
print("SessionId", "=", lsid_str)
297+
session_fields_to_print = ['_sessionId', '_checkoutOpCtx', '_killRequested']
298+
for field in session_fields_to_print:
299+
print(field, "=", session[field])
300+
301+
# Print the information from a TransactionParticipant if a session has one. Otherwise
302+
# we just print the session's id and nothing else.
303+
txn_part_dec = get_decoration(session, "TransactionParticipant")
304+
if txn_part_dec:
305+
# Only print the most interesting fields for debugging transactions issues.
306+
txn_part = txn_part_dec[1]
307+
fields_to_print = ['_txnState', '_activeTxnNumber']
308+
print("TransactionParticipant (" + str(txn_part.address) + "):")
309+
for field in fields_to_print:
310+
print(field, "=", txn_part[field])
311+
312+
# The '_txnResourceStash' field is a boost::optional so we unpack it
313+
# manually if it is non-empty. We are only interested in its Locker object for now.
314+
# TODO: Load the boost pretty printers so the object will be printed clearly
315+
# by default, without the need for special unpacking.
316+
val = get_boost_optional(txn_part['_txnResourceStash'])
317+
if val:
318+
locker_addr = val["_locker"]["_M_t"]['_M_head_impl']
319+
print('_txnResourceStash._locker', "@", locker_addr)
320+
else:
321+
print('_txnResourceStash', "=", None)
322+
# Separate sessions by a newline.
323+
print("")
324+
325+
326+
# Register command
327+
DumpMongoDSessionCatalog()
328+
329+
94330
class MongoDBDumpLocks(gdb.Command):
95331
"""Dump locks in mongod process."""
96332

buildscripts/gdb/mongo_printers.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import struct
66
import sys
7+
import uuid
78

89
import gdb.printing
910

@@ -17,6 +18,12 @@
1718
print("Check with the pip command if pymongo 3.x is installed.")
1819
bson = None
1920

21+
if sys.version_info[0] >= 3:
22+
# GDB only permits converting a gdb.Value instance to its numerical address when using the
23+
# long() constructor in Python 2 and not when using the int() constructor. We define the
24+
# 'long' class as an alias for the 'int' class in Python 3 for compatibility.
25+
long = int # pylint: disable=redefined-builtin,invalid-name
26+
2027

2128
def get_unique_ptr(obj):
2229
"""Read the value of a libstdc++ std::unique_ptr."""
@@ -147,6 +154,25 @@ def to_string(self):
147154
return "%s BSONObj %s bytes @ %s" % (ownership, size, self.ptr)
148155

149156

157+
class UUIDPrinter(object):
158+
"""Pretty-printer for mongo::UUID."""
159+
160+
def __init__(self, val):
161+
"""Initialize UUIDPrinter."""
162+
self.val = val
163+
164+
@staticmethod
165+
def display_hint():
166+
"""Display hint."""
167+
return 'string'
168+
169+
def to_string(self):
170+
"""Return UUID for printing."""
171+
raw_bytes = [self.val['_uuid']['_M_elems'][i] for i in range(16)]
172+
uuid_hex_bytes = [hex(int(b))[2:].zfill(2) for b in raw_bytes]
173+
return str(uuid.UUID("".join(uuid_hex_bytes)))
174+
175+
150176
class UnorderedFastKeyTablePrinter(object):
151177
"""Pretty-printer for mongo::UnorderedFastKeyTable<>."""
152178

@@ -204,7 +230,7 @@ def __init__(self, val):
204230
decorable_t = val.type.template_argument(0)
205231
decinfo_t = gdb.lookup_type(
206232
'mongo::DecorationRegistry<{}>::DecorationInfo'.format(decorable_t))
207-
self.count = int((int(finish) - int(self.start)) / decinfo_t.sizeof)
233+
self.count = long((long(finish) - long(self.start)) / decinfo_t.sizeof)
208234

209235
@staticmethod
210236
def display_hint():
@@ -462,6 +488,7 @@ def build_pretty_printer():
462488
pp.add('StringData', 'mongo::StringData', False, StringDataPrinter)
463489
pp.add('UnorderedFastKeyTable', 'mongo::UnorderedFastKeyTable', True,
464490
UnorderedFastKeyTablePrinter)
491+
pp.add('UUID', 'mongo::UUID', False, UUIDPrinter)
465492
pp.add('__wt_cursor', '__wt_cursor', False, WtCursorPrinter)
466493
pp.add('__wt_session_impl', '__wt_session_impl', False, WtSessionImplPrinter)
467494
pp.add('__wt_txn', '__wt_txn', False, WtTxnPrinter)

buildscripts/hang_analyzer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ def dump_info( # pylint: disable=too-many-arguments,too-many-locals
330330
mongodb_waitsfor_graph = "mongodb-waitsfor-graph debugger_waitsfor_%s_%d.gv" % \
331331
(process_name, pid)
332332
mongodb_javascript_stack = "mongodb-javascript-stack"
333+
mongod_dump_sessions = "mongod-dump-sessions"
333334

334335
# The following MongoDB python extensions do not run on Solaris.
335336
if sys.platform.startswith("sunos"):
@@ -342,6 +343,7 @@ def dump_info( # pylint: disable=too-many-arguments,too-many-locals
342343
mongodb_show_locks = ""
343344
mongodb_waitsfor_graph = ""
344345
mongodb_javascript_stack = ""
346+
mongod_dump_sessions = ""
345347

346348
if not logger.mongo_process_filename:
347349
raw_stacks_commands = []
@@ -378,6 +380,7 @@ def dump_info( # pylint: disable=too-many-arguments,too-many-locals
378380
mongodb_show_locks,
379381
mongodb_waitsfor_graph,
380382
mongodb_javascript_stack,
383+
mongod_dump_sessions,
381384
"set confirm off",
382385
"quit",
383386
]

0 commit comments

Comments
 (0)