You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.go 19 kB

4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. // Copyright (C) MongoDB, Inc. 2017-present.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. // not use this file except in compliance with the License. You may obtain
  5. // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. package mongo
  7. import (
  8. "context"
  9. "crypto/tls"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "go.mongodb.org/mongo-driver/bson"
  14. "go.mongodb.org/mongo-driver/bson/bsoncodec"
  15. "go.mongodb.org/mongo-driver/event"
  16. "go.mongodb.org/mongo-driver/mongo/options"
  17. "go.mongodb.org/mongo-driver/mongo/readconcern"
  18. "go.mongodb.org/mongo-driver/mongo/readpref"
  19. "go.mongodb.org/mongo-driver/mongo/writeconcern"
  20. "go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
  21. "go.mongodb.org/mongo-driver/x/mongo/driver"
  22. "go.mongodb.org/mongo-driver/x/mongo/driver/auth"
  23. "go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
  24. "go.mongodb.org/mongo-driver/x/mongo/driver/description"
  25. "go.mongodb.org/mongo-driver/x/mongo/driver/operation"
  26. "go.mongodb.org/mongo-driver/x/mongo/driver/session"
  27. "go.mongodb.org/mongo-driver/x/mongo/driver/topology"
  28. "go.mongodb.org/mongo-driver/x/mongo/driver/uuid"
  29. )
  30. const defaultLocalThreshold = 15 * time.Millisecond
  31. const batchSize = 10000
  32. // Client performs operations on a given topology.
  33. type Client struct {
  34. id uuid.UUID
  35. topologyOptions []topology.Option
  36. topology *topology.Topology
  37. connString connstring.ConnString
  38. localThreshold time.Duration
  39. retryWrites bool
  40. retryReads bool
  41. clock *session.ClusterClock
  42. readPreference *readpref.ReadPref
  43. readConcern *readconcern.ReadConcern
  44. writeConcern *writeconcern.WriteConcern
  45. registry *bsoncodec.Registry
  46. marshaller BSONAppender
  47. monitor *event.CommandMonitor
  48. }
  49. // Connect creates a new Client and then initializes it using the Connect method.
  50. func Connect(ctx context.Context, opts ...*options.ClientOptions) (*Client, error) {
  51. c, err := NewClient(opts...)
  52. if err != nil {
  53. return nil, err
  54. }
  55. err = c.Connect(ctx)
  56. if err != nil {
  57. return nil, err
  58. }
  59. return c, nil
  60. }
  61. // NewClient creates a new client to connect to a cluster specified by the uri.
  62. //
  63. // When creating an options.ClientOptions, the order the methods are called matters. Later Set*
  64. // methods will overwrite the values from previous Set* method invocations. This includes the
  65. // ApplyURI method. This allows callers to determine the order of precedence for option
  66. // application. For instance, if ApplyURI is called before SetAuth, the Credential from
  67. // SetAuth will overwrite the values from the connection string. If ApplyURI is called
  68. // after SetAuth, then its values will overwrite those from SetAuth.
  69. //
  70. // The opts parameter is processed using options.MergeClientOptions, which will overwrite entire
  71. // option fields of previous options, there is no partial overwriting. For example, if Username is
  72. // set in the Auth field for the first option, and Password is set for the second but with no
  73. // Username, after the merge the Username field will be empty.
  74. func NewClient(opts ...*options.ClientOptions) (*Client, error) {
  75. clientOpt := options.MergeClientOptions(opts...)
  76. id, err := uuid.New()
  77. if err != nil {
  78. return nil, err
  79. }
  80. client := &Client{id: id}
  81. err = client.configure(clientOpt)
  82. if err != nil {
  83. return nil, err
  84. }
  85. client.topology, err = topology.New(client.topologyOptions...)
  86. if err != nil {
  87. return nil, replaceErrors(err)
  88. }
  89. return client, nil
  90. }
  91. // Connect initializes the Client by starting background monitoring goroutines.
  92. // This method must be called before a Client can be used.
  93. func (c *Client) Connect(ctx context.Context) error {
  94. err := c.topology.Connect()
  95. if err != nil {
  96. return replaceErrors(err)
  97. }
  98. return nil
  99. }
  100. // Disconnect closes sockets to the topology referenced by this Client. It will
  101. // shut down any monitoring goroutines, close the idle connection pool, and will
  102. // wait until all the in use connections have been returned to the connection
  103. // pool and closed before returning. If the context expires via cancellation,
  104. // deadline, or timeout before the in use connections have returned, the in use
  105. // connections will be closed, resulting in the failure of any in flight read
  106. // or write operations. If this method returns with no errors, all connections
  107. // associated with this Client have been closed.
  108. func (c *Client) Disconnect(ctx context.Context) error {
  109. if ctx == nil {
  110. ctx = context.Background()
  111. }
  112. c.endSessions(ctx)
  113. return replaceErrors(c.topology.Disconnect(ctx))
  114. }
  115. // Ping verifies that the client can connect to the topology.
  116. // If readPreference is nil then will use the client's default read
  117. // preference.
  118. func (c *Client) Ping(ctx context.Context, rp *readpref.ReadPref) error {
  119. if ctx == nil {
  120. ctx = context.Background()
  121. }
  122. if rp == nil {
  123. rp = c.readPreference
  124. }
  125. db := c.Database("admin")
  126. res := db.RunCommand(ctx, bson.D{
  127. {"ping", 1},
  128. }, options.RunCmd().SetReadPreference(rp))
  129. return replaceErrors(res.Err())
  130. }
  131. // StartSession starts a new session.
  132. func (c *Client) StartSession(opts ...*options.SessionOptions) (Session, error) {
  133. if c.topology.SessionPool == nil {
  134. return nil, ErrClientDisconnected
  135. }
  136. sopts := options.MergeSessionOptions(opts...)
  137. coreOpts := &session.ClientOptions{
  138. DefaultReadConcern: c.readConcern,
  139. DefaultReadPreference: c.readPreference,
  140. DefaultWriteConcern: c.writeConcern,
  141. }
  142. if sopts.CausalConsistency != nil {
  143. coreOpts.CausalConsistency = sopts.CausalConsistency
  144. }
  145. if sopts.DefaultReadConcern != nil {
  146. coreOpts.DefaultReadConcern = sopts.DefaultReadConcern
  147. }
  148. if sopts.DefaultWriteConcern != nil {
  149. coreOpts.DefaultWriteConcern = sopts.DefaultWriteConcern
  150. }
  151. if sopts.DefaultReadPreference != nil {
  152. coreOpts.DefaultReadPreference = sopts.DefaultReadPreference
  153. }
  154. if sopts.DefaultMaxCommitTime != nil {
  155. coreOpts.DefaultMaxCommitTime = sopts.DefaultMaxCommitTime
  156. }
  157. sess, err := session.NewClientSession(c.topology.SessionPool, c.id, session.Explicit, coreOpts)
  158. if err != nil {
  159. return nil, replaceErrors(err)
  160. }
  161. sess.RetryWrite = c.retryWrites
  162. sess.RetryRead = c.retryReads
  163. return &sessionImpl{
  164. clientSession: sess,
  165. client: c,
  166. topo: c.topology,
  167. }, nil
  168. }
  169. func (c *Client) endSessions(ctx context.Context) {
  170. if c.topology.SessionPool == nil {
  171. return
  172. }
  173. ids := c.topology.SessionPool.IDSlice()
  174. idx, idArray := bsoncore.AppendArrayStart(nil)
  175. for i, id := range ids {
  176. idDoc, _ := id.MarshalBSON()
  177. idArray = bsoncore.AppendDocumentElement(idArray, strconv.Itoa(i), idDoc)
  178. }
  179. idArray, _ = bsoncore.AppendArrayEnd(idArray, idx)
  180. op := operation.NewEndSessions(idArray).ClusterClock(c.clock).Deployment(c.topology).
  181. ServerSelector(description.ReadPrefSelector(readpref.PrimaryPreferred())).CommandMonitor(c.monitor).Database("admin")
  182. idx, idArray = bsoncore.AppendArrayStart(nil)
  183. totalNumIDs := len(ids)
  184. for i := 0; i < totalNumIDs; i++ {
  185. idDoc, _ := ids[i].MarshalBSON()
  186. idArray = bsoncore.AppendDocumentElement(idArray, strconv.Itoa(i), idDoc)
  187. if ((i+1)%batchSize) == 0 || i == totalNumIDs-1 {
  188. idArray, _ = bsoncore.AppendArrayEnd(idArray, idx)
  189. _ = op.SessionIDs(idArray).Execute(ctx)
  190. idArray = idArray[:0]
  191. idx = 0
  192. }
  193. }
  194. }
  195. func (c *Client) configure(opts *options.ClientOptions) error {
  196. if err := opts.Validate(); err != nil {
  197. return err
  198. }
  199. var connOpts []topology.ConnectionOption
  200. var serverOpts []topology.ServerOption
  201. var topologyOpts []topology.Option
  202. // TODO(GODRIVER-814): Add tests for topology, server, and connection related options.
  203. // AppName
  204. var appName string
  205. if opts.AppName != nil {
  206. appName = *opts.AppName
  207. }
  208. // Compressors & ZlibLevel
  209. var comps []string
  210. if len(opts.Compressors) > 0 {
  211. comps = opts.Compressors
  212. connOpts = append(connOpts, topology.WithCompressors(
  213. func(compressors []string) []string {
  214. return append(compressors, comps...)
  215. },
  216. ))
  217. for _, comp := range comps {
  218. if comp == "zlib" {
  219. connOpts = append(connOpts, topology.WithZlibLevel(func(level *int) *int {
  220. return opts.ZlibLevel
  221. }))
  222. }
  223. }
  224. serverOpts = append(serverOpts, topology.WithCompressionOptions(
  225. func(opts ...string) []string { return append(opts, comps...) },
  226. ))
  227. }
  228. // Handshaker
  229. var handshaker = func(driver.Handshaker) driver.Handshaker {
  230. return operation.NewIsMaster().AppName(appName).Compressors(comps)
  231. }
  232. // Auth & Database & Password & Username
  233. if opts.Auth != nil {
  234. cred := &auth.Cred{
  235. Username: opts.Auth.Username,
  236. Password: opts.Auth.Password,
  237. PasswordSet: opts.Auth.PasswordSet,
  238. Props: opts.Auth.AuthMechanismProperties,
  239. Source: opts.Auth.AuthSource,
  240. }
  241. mechanism := opts.Auth.AuthMechanism
  242. if len(cred.Source) == 0 {
  243. switch strings.ToUpper(mechanism) {
  244. case auth.MongoDBX509, auth.GSSAPI, auth.PLAIN:
  245. cred.Source = "$external"
  246. default:
  247. cred.Source = "admin"
  248. }
  249. }
  250. authenticator, err := auth.CreateAuthenticator(mechanism, cred)
  251. if err != nil {
  252. return err
  253. }
  254. handshakeOpts := &auth.HandshakeOptions{
  255. AppName: appName,
  256. Authenticator: authenticator,
  257. Compressors: comps,
  258. }
  259. if mechanism == "" {
  260. // Required for SASL mechanism negotiation during handshake
  261. handshakeOpts.DBUser = cred.Source + "." + cred.Username
  262. }
  263. if opts.AuthenticateToAnything != nil && *opts.AuthenticateToAnything {
  264. // Authenticate arbiters
  265. handshakeOpts.PerformAuthentication = func(serv description.Server) bool {
  266. return true
  267. }
  268. }
  269. handshaker = func(driver.Handshaker) driver.Handshaker {
  270. return auth.Handshaker(nil, handshakeOpts)
  271. }
  272. }
  273. connOpts = append(connOpts, topology.WithHandshaker(handshaker))
  274. // ConnectTimeout
  275. if opts.ConnectTimeout != nil {
  276. serverOpts = append(serverOpts, topology.WithHeartbeatTimeout(
  277. func(time.Duration) time.Duration { return *opts.ConnectTimeout },
  278. ))
  279. connOpts = append(connOpts, topology.WithConnectTimeout(
  280. func(time.Duration) time.Duration { return *opts.ConnectTimeout },
  281. ))
  282. }
  283. // Dialer
  284. if opts.Dialer != nil {
  285. connOpts = append(connOpts, topology.WithDialer(
  286. func(topology.Dialer) topology.Dialer { return opts.Dialer },
  287. ))
  288. }
  289. // Direct
  290. if opts.Direct != nil && *opts.Direct {
  291. topologyOpts = append(topologyOpts, topology.WithMode(
  292. func(topology.MonitorMode) topology.MonitorMode { return topology.SingleMode },
  293. ))
  294. }
  295. // HeartbeatInterval
  296. if opts.HeartbeatInterval != nil {
  297. serverOpts = append(serverOpts, topology.WithHeartbeatInterval(
  298. func(time.Duration) time.Duration { return *opts.HeartbeatInterval },
  299. ))
  300. }
  301. // Hosts
  302. hosts := []string{"localhost:27017"} // default host
  303. if len(opts.Hosts) > 0 {
  304. hosts = opts.Hosts
  305. }
  306. topologyOpts = append(topologyOpts, topology.WithSeedList(
  307. func(...string) []string { return hosts },
  308. ))
  309. // LocalThreshold
  310. c.localThreshold = defaultLocalThreshold
  311. if opts.LocalThreshold != nil {
  312. c.localThreshold = *opts.LocalThreshold
  313. }
  314. // MaxConIdleTime
  315. if opts.MaxConnIdleTime != nil {
  316. connOpts = append(connOpts, topology.WithIdleTimeout(
  317. func(time.Duration) time.Duration { return *opts.MaxConnIdleTime },
  318. ))
  319. }
  320. // MaxPoolSize
  321. if opts.MaxPoolSize != nil {
  322. serverOpts = append(
  323. serverOpts,
  324. topology.WithMaxConnections(func(uint64) uint64 { return *opts.MaxPoolSize }),
  325. )
  326. }
  327. // MinPoolSize
  328. if opts.MinPoolSize != nil {
  329. serverOpts = append(
  330. serverOpts,
  331. topology.WithMinConnections(func(uint64) uint64 { return *opts.MinPoolSize }),
  332. )
  333. }
  334. // PoolMonitor
  335. if opts.PoolMonitor != nil {
  336. serverOpts = append(
  337. serverOpts,
  338. topology.WithConnectionPoolMonitor(func(*event.PoolMonitor) *event.PoolMonitor { return opts.PoolMonitor }),
  339. )
  340. }
  341. // Monitor
  342. if opts.Monitor != nil {
  343. c.monitor = opts.Monitor
  344. connOpts = append(connOpts, topology.WithMonitor(
  345. func(*event.CommandMonitor) *event.CommandMonitor { return opts.Monitor },
  346. ))
  347. }
  348. // ReadConcern
  349. c.readConcern = readconcern.New()
  350. if opts.ReadConcern != nil {
  351. c.readConcern = opts.ReadConcern
  352. }
  353. // ReadPreference
  354. c.readPreference = readpref.Primary()
  355. if opts.ReadPreference != nil {
  356. c.readPreference = opts.ReadPreference
  357. }
  358. // Registry
  359. c.registry = bson.DefaultRegistry
  360. if opts.Registry != nil {
  361. c.registry = opts.Registry
  362. }
  363. // ReplicaSet
  364. if opts.ReplicaSet != nil {
  365. topologyOpts = append(topologyOpts, topology.WithReplicaSetName(
  366. func(string) string { return *opts.ReplicaSet },
  367. ))
  368. }
  369. // RetryWrites
  370. c.retryWrites = true // retry writes on by default
  371. if opts.RetryWrites != nil {
  372. c.retryWrites = *opts.RetryWrites
  373. }
  374. c.retryReads = true
  375. if opts.RetryReads != nil {
  376. c.retryReads = *opts.RetryReads
  377. }
  378. // ServerSelectionTimeout
  379. if opts.ServerSelectionTimeout != nil {
  380. topologyOpts = append(topologyOpts, topology.WithServerSelectionTimeout(
  381. func(time.Duration) time.Duration { return *opts.ServerSelectionTimeout },
  382. ))
  383. }
  384. // SocketTimeout
  385. if opts.SocketTimeout != nil {
  386. connOpts = append(
  387. connOpts,
  388. topology.WithReadTimeout(func(time.Duration) time.Duration { return *opts.SocketTimeout }),
  389. topology.WithWriteTimeout(func(time.Duration) time.Duration { return *opts.SocketTimeout }),
  390. )
  391. }
  392. // TLSConfig
  393. if opts.TLSConfig != nil {
  394. connOpts = append(connOpts, topology.WithTLSConfig(
  395. func(*tls.Config) *tls.Config {
  396. return opts.TLSConfig
  397. },
  398. ))
  399. }
  400. // WriteConcern
  401. if opts.WriteConcern != nil {
  402. c.writeConcern = opts.WriteConcern
  403. }
  404. // ClusterClock
  405. c.clock = new(session.ClusterClock)
  406. serverOpts = append(
  407. serverOpts,
  408. topology.WithClock(func(*session.ClusterClock) *session.ClusterClock { return c.clock }),
  409. topology.WithConnectionOptions(func(...topology.ConnectionOption) []topology.ConnectionOption { return connOpts }),
  410. )
  411. c.topologyOptions = append(topologyOpts, topology.WithServerOptions(
  412. func(...topology.ServerOption) []topology.ServerOption { return serverOpts },
  413. ))
  414. return nil
  415. }
  416. // validSession returns an error if the session doesn't belong to the client
  417. func (c *Client) validSession(sess *session.Client) error {
  418. if sess != nil && !uuid.Equal(sess.ClientID, c.id) {
  419. return ErrWrongClient
  420. }
  421. return nil
  422. }
  423. // Database returns a handle for a given database.
  424. func (c *Client) Database(name string, opts ...*options.DatabaseOptions) *Database {
  425. return newDatabase(c, name, opts...)
  426. }
  427. // ListDatabases returns a ListDatabasesResult.
  428. func (c *Client) ListDatabases(ctx context.Context, filter interface{}, opts ...*options.ListDatabasesOptions) (ListDatabasesResult, error) {
  429. if ctx == nil {
  430. ctx = context.Background()
  431. }
  432. sess := sessionFromContext(ctx)
  433. err := c.validSession(sess)
  434. if sess == nil && c.topology.SessionPool != nil {
  435. sess, err = session.NewClientSession(c.topology.SessionPool, c.id, session.Implicit)
  436. if err != nil {
  437. return ListDatabasesResult{}, err
  438. }
  439. defer sess.EndSession()
  440. }
  441. err = c.validSession(sess)
  442. if err != nil {
  443. return ListDatabasesResult{}, err
  444. }
  445. filterDoc, err := transformBsoncoreDocument(c.registry, filter)
  446. if err != nil {
  447. return ListDatabasesResult{}, err
  448. }
  449. selector := makePinnedSelector(sess, description.CompositeSelector([]description.ServerSelector{
  450. description.ReadPrefSelector(readpref.Primary()),
  451. description.LatencySelector(c.localThreshold),
  452. }))
  453. ldo := options.MergeListDatabasesOptions(opts...)
  454. op := operation.NewListDatabases(filterDoc).
  455. Session(sess).ReadPreference(c.readPreference).CommandMonitor(c.monitor).
  456. ServerSelector(selector).ClusterClock(c.clock).Database("admin").Deployment(c.topology)
  457. if ldo.NameOnly != nil {
  458. op = op.NameOnly(*ldo.NameOnly)
  459. }
  460. retry := driver.RetryNone
  461. if c.retryReads {
  462. retry = driver.RetryOncePerCommand
  463. }
  464. op.Retry(retry)
  465. err = op.Execute(ctx)
  466. if err != nil {
  467. return ListDatabasesResult{}, replaceErrors(err)
  468. }
  469. return newListDatabasesResultFromOperation(op.Result()), nil
  470. }
  471. // ListDatabaseNames returns a slice containing the names of all of the databases on the server.
  472. func (c *Client) ListDatabaseNames(ctx context.Context, filter interface{}, opts ...*options.ListDatabasesOptions) ([]string, error) {
  473. opts = append(opts, options.ListDatabases().SetNameOnly(true))
  474. res, err := c.ListDatabases(ctx, filter, opts...)
  475. if err != nil {
  476. return nil, err
  477. }
  478. names := make([]string, 0)
  479. for _, spec := range res.Databases {
  480. names = append(names, spec.Name)
  481. }
  482. return names, nil
  483. }
  484. // WithSession allows a user to start a session themselves and manage
  485. // its lifetime. The only way to provide a session to a CRUD method is
  486. // to invoke that CRUD method with the mongo.SessionContext within the
  487. // closure. The mongo.SessionContext can be used as a regular context,
  488. // so methods like context.WithDeadline and context.WithTimeout are
  489. // supported.
  490. //
  491. // If the context.Context already has a mongo.Session attached, that
  492. // mongo.Session will be replaced with the one provided.
  493. //
  494. // Errors returned from the closure are transparently returned from
  495. // this function.
  496. func WithSession(ctx context.Context, sess Session, fn func(SessionContext) error) error {
  497. return fn(contextWithSession(ctx, sess))
  498. }
  499. // UseSession creates a default session, that is only valid for the
  500. // lifetime of the closure. No cleanup outside of closing the session
  501. // is done upon exiting the closure. This means that an outstanding
  502. // transaction will be aborted, even if the closure returns an error.
  503. //
  504. // If ctx already contains a mongo.Session, that mongo.Session will be
  505. // replaced with the newly created mongo.Session.
  506. //
  507. // Errors returned from the closure are transparently returned from
  508. // this method.
  509. func (c *Client) UseSession(ctx context.Context, fn func(SessionContext) error) error {
  510. return c.UseSessionWithOptions(ctx, options.Session(), fn)
  511. }
  512. // UseSessionWithOptions works like UseSession but allows the caller
  513. // to specify the options used to create the session.
  514. func (c *Client) UseSessionWithOptions(ctx context.Context, opts *options.SessionOptions, fn func(SessionContext) error) error {
  515. defaultSess, err := c.StartSession(opts)
  516. if err != nil {
  517. return err
  518. }
  519. defer defaultSess.EndSession(ctx)
  520. sessCtx := sessionContext{
  521. Context: context.WithValue(ctx, sessionKey{}, defaultSess),
  522. Session: defaultSess,
  523. }
  524. return fn(sessCtx)
  525. }
  526. // Watch returns a change stream cursor used to receive information of changes to the client. This method is preferred
  527. // to running a raw aggregation with a $changeStream stage because it supports resumability in the case of some errors.
  528. // The client must have read concern majority or no read concern for a change stream to be created successfully.
  529. func (c *Client) Watch(ctx context.Context, pipeline interface{},
  530. opts ...*options.ChangeStreamOptions) (*ChangeStream, error) {
  531. if c.topology.SessionPool == nil {
  532. return nil, ErrClientDisconnected
  533. }
  534. csConfig := changeStreamConfig{
  535. readConcern: c.readConcern,
  536. readPreference: c.readPreference,
  537. client: c,
  538. registry: c.registry,
  539. streamType: ClientStream,
  540. }
  541. return newChangeStream(ctx, csConfig, pipeline, opts...)
  542. }