github.com/openshift-online/ocm-sdk-go@v0.1.473/leadership/flag_test.go (about)

     1  /*
     2  Copyright (c) 2021 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package leadership
    18  
    19  import (
    20  	"context"
    21  	"database/sql"
    22  	"fmt"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	. "github.com/onsi/ginkgo/v2/dsl/core"             // nolint
    27  	. "github.com/onsi/gomega"                         // nolint
    28  	. "github.com/openshift-online/ocm-sdk-go/testing" // nolint
    29  )
    30  
    31  var _ = Describe("Flag behaviour", func() {
    32  	var ctx context.Context
    33  	var dbObject *Database
    34  	var dbHandle *sql.DB
    35  
    36  	var CreateTable = func() {
    37  		_, err := dbHandle.Exec(`
    38  			create table leadership_flags (
    39  				name text not null primary key,
    40  				holder text not null,
    41  				version bigint not null,
    42  				timestamp timestamp with time zone not null
    43  			)
    44  		`)
    45  		Expect(err).ToNot(HaveOccurred())
    46  	}
    47  
    48  	BeforeEach(func() {
    49  		// Create a context:
    50  		ctx = context.Background()
    51  
    52  		// Create a database:
    53  		dbObject = dbServer.MakeDatabase()
    54  		dbHandle = dbObject.MakeHandle()
    55  	})
    56  
    57  	AfterEach(func() {
    58  		dbObject.Close()
    59  	})
    60  
    61  	It("Can't be created without a logger", func() {
    62  		_, err := NewFlag().
    63  			Handle(dbHandle).
    64  			Name("my_flag").
    65  			Process("my_process").
    66  			Build(ctx)
    67  		Expect(err).To(HaveOccurred())
    68  		message := err.Error()
    69  		Expect(message).To(ContainSubstring("logger"))
    70  		Expect(message).To(ContainSubstring("mandatory"))
    71  	})
    72  
    73  	It("Can't be created without a database handle", func() {
    74  		_, err := NewFlag().
    75  			Logger(logger).
    76  			Name("my_flag").
    77  			Process("my_process").
    78  			Build(ctx)
    79  		Expect(err).To(HaveOccurred())
    80  		message := err.Error()
    81  		Expect(message).To(ContainSubstring("database"))
    82  		Expect(message).To(ContainSubstring("handle"))
    83  		Expect(message).To(ContainSubstring("mandatory"))
    84  	})
    85  
    86  	It("Can't be created without a name", func() {
    87  		_, err := NewFlag().
    88  			Logger(logger).
    89  			Handle(dbHandle).
    90  			Process("my_process").
    91  			Build(ctx)
    92  		Expect(err).To(HaveOccurred())
    93  		message := err.Error()
    94  		Expect(message).To(ContainSubstring("name"))
    95  		Expect(message).To(ContainSubstring("mandatory"))
    96  	})
    97  
    98  	It("Can't be created without a process name", func() {
    99  		_, err := NewFlag().
   100  			Logger(logger).
   101  			Handle(dbHandle).
   102  			Name("my_flag").
   103  			Build(ctx)
   104  		Expect(err).To(HaveOccurred())
   105  		message := err.Error()
   106  		Expect(message).To(ContainSubstring("process"))
   107  		Expect(message).To(ContainSubstring("mandatory"))
   108  	})
   109  
   110  	It("Creates the table if it doesn't exist", func() {
   111  		// Create the flag:
   112  		flag, err := NewFlag().
   113  			Logger(logger).
   114  			Handle(dbHandle).
   115  			Name("my_flag").
   116  			Process("my_process").
   117  			Build(ctx)
   118  		Expect(err).ToNot(HaveOccurred())
   119  		defer func() {
   120  			err = flag.Close()
   121  			Expect(err).ToNot(HaveOccurred())
   122  		}()
   123  
   124  		// Check that the table exists:
   125  		rows, err := dbHandle.Query(`
   126  			select
   127  				name,
   128  				holder,
   129  				version,
   130  				timestamp
   131  			from
   132  				leadership_flags
   133  		`)
   134  		Expect(err).ToNot(HaveOccurred())
   135  		err = rows.Close()
   136  		Expect(err).ToNot(HaveOccurred())
   137  	})
   138  
   139  	It("Can be created if the table already exists", func() {
   140  		// Create the database table:
   141  		CreateTable()
   142  
   143  		// Create the flag object:
   144  		flag, err := NewFlag().
   145  			Logger(logger).
   146  			Handle(dbHandle).
   147  			Name("my_flag").
   148  			Process("my_process").
   149  			Build(ctx)
   150  		Expect(err).ToNot(HaveOccurred())
   151  		defer func() {
   152  			err = flag.Close()
   153  			Expect(err).ToNot(HaveOccurred())
   154  		}()
   155  	})
   156  
   157  	When("Doesn't exist", func() {
   158  		var flag *Flag
   159  
   160  		BeforeEach(func() {
   161  			var err error
   162  
   163  			// Create the flag object:
   164  			flag, err = NewFlag().
   165  				Logger(logger).
   166  				Handle(dbHandle).
   167  				Name("my_flag").
   168  				Process("my_process").
   169  				Interval(200 * time.Millisecond).
   170  				Build(ctx)
   171  			Expect(err).ToNot(HaveOccurred())
   172  		})
   173  
   174  		AfterEach(func() {
   175  			err := flag.Close()
   176  			Expect(err).ToNot(HaveOccurred())
   177  		})
   178  
   179  		It("It is quickly raised ", func() {
   180  			time.Sleep(40 * time.Millisecond)
   181  			Expect(flag.Raised()).To(BeTrue())
   182  		})
   183  	})
   184  
   185  	When("Is held by another process and not expired", func() {
   186  		var flag *Flag
   187  
   188  		BeforeEach(func() {
   189  			var err error
   190  
   191  			// Create the database table:
   192  			CreateTable()
   193  
   194  			// Create the database row so that the flag is already held by another
   195  			// process that will eventually fail to update it:
   196  			_, err = dbHandle.Exec(`
   197  				insert into leadership_flags (
   198  					name,
   199  					holder,
   200  					version,
   201  					timestamp
   202  				) values (
   203  					'my_flag',
   204  					'your_process',
   205  					123,
   206  					now()
   207  				)
   208  			`)
   209  			Expect(err).ToNot(HaveOccurred())
   210  
   211  			// Create the object:
   212  			flag, err = NewFlag().
   213  				Logger(logger).
   214  				Handle(dbHandle).
   215  				Name("my_flag").
   216  				Process("my_process").
   217  				Interval(200 * time.Millisecond).
   218  				Jitter(0).
   219  				Build(ctx)
   220  			Expect(err).ToNot(HaveOccurred())
   221  		})
   222  
   223  		AfterEach(func() {
   224  			err := flag.Close()
   225  			Expect(err).ToNot(HaveOccurred())
   226  		})
   227  
   228  		It("It isn't quickly raised", func() {
   229  			time.Sleep(40 * time.Millisecond)
   230  			Expect(flag.Raised()).To(BeFalse())
   231  		})
   232  
   233  		It("It isn't raised while the previous holder can still renew", func() {
   234  			time.Sleep(100 * time.Millisecond)
   235  			Expect(flag.Raised()).To(BeFalse())
   236  		})
   237  
   238  		It("It is raised when the previous holder fails to renew", func() {
   239  			time.Sleep(400 * time.Millisecond)
   240  			Expect(flag.Raised()).To(BeTrue())
   241  		})
   242  	})
   243  
   244  	When("Is held by another process but already expired", func() {
   245  		var flag *Flag
   246  
   247  		BeforeEach(func() {
   248  			var err error
   249  
   250  			// Create the database table:
   251  			CreateTable()
   252  
   253  			// Create the database row so that the flag is already held by another
   254  			// process that already failed to renew it:
   255  			_, err = dbHandle.Exec(`
   256  				insert into leadership_flags (
   257  					name,
   258  					holder,
   259  					version,
   260  					timestamp
   261  				) values (
   262  					'my_flag',
   263  					'your_process',
   264  					123,
   265  					now() - interval '1 second'
   266  				)
   267  			`)
   268  			Expect(err).ToNot(HaveOccurred())
   269  
   270  			// Create the object:
   271  			flag, err = NewFlag().
   272  				Logger(logger).
   273  				Handle(dbHandle).
   274  				Name("my_flag").
   275  				Process("my_process").
   276  				Interval(200 * time.Millisecond).
   277  				Jitter(0).
   278  				Build(ctx)
   279  			Expect(err).ToNot(HaveOccurred())
   280  		})
   281  
   282  		AfterEach(func() {
   283  			err := flag.Close()
   284  			Expect(err).ToNot(HaveOccurred())
   285  		})
   286  
   287  		It("It is quickly raised", func() {
   288  			time.Sleep(40 * time.Millisecond)
   289  			Expect(flag.Raised()).To(BeTrue())
   290  		})
   291  	})
   292  
   293  	When("Is held by this process and not expired", func() {
   294  		var flag *Flag
   295  
   296  		BeforeEach(func() {
   297  			var err error
   298  
   299  			// Create the database table:
   300  			CreateTable()
   301  
   302  			// Create the database row so that the flag is already held by this process:
   303  			_, err = dbHandle.Exec(`
   304  				insert into leadership_flags (
   305  					name,
   306  					holder,
   307  					version,
   308  					timestamp
   309  				) values (
   310  					'my_flag',
   311  					'my_process',
   312  					123,
   313  					now()
   314  				)
   315  			`)
   316  			Expect(err).ToNot(HaveOccurred())
   317  
   318  			// Create the object:
   319  			flag, err = NewFlag().
   320  				Logger(logger).
   321  				Handle(dbHandle).
   322  				Name("my_flag").
   323  				Process("my_process").
   324  				Interval(200 * time.Millisecond).
   325  				Jitter(0).
   326  				Build(ctx)
   327  			Expect(err).ToNot(HaveOccurred())
   328  		})
   329  
   330  		AfterEach(func() {
   331  			err := flag.Close()
   332  			Expect(err).ToNot(HaveOccurred())
   333  		})
   334  
   335  		It("It is quickly raised", func() {
   336  			time.Sleep(40 * time.Millisecond)
   337  			Expect(flag.Raised()).To(BeTrue())
   338  		})
   339  	})
   340  
   341  	When("Is held by this process and already expired", func() {
   342  		var flag *Flag
   343  
   344  		BeforeEach(func() {
   345  			var err error
   346  
   347  			// Create the database table:
   348  			CreateTable()
   349  
   350  			// Create the database row so that the flag is held by this process but
   351  			// expired:
   352  			_, err = dbHandle.Exec(`
   353  				insert into leadership_flags (
   354  					name,
   355  					holder,
   356  					version,
   357  					timestamp
   358  				) values (
   359  					'my_flag',
   360  					'my_process',
   361  					123,
   362  					now() - interval '1 second'
   363  				)
   364  			`)
   365  			Expect(err).ToNot(HaveOccurred())
   366  
   367  			// Create the object:
   368  			flag, err = NewFlag().
   369  				Logger(logger).
   370  				Handle(dbHandle).
   371  				Name("my_flag").
   372  				Process("my_process").
   373  				Interval(200 * time.Millisecond).
   374  				Jitter(0).
   375  				Build(ctx)
   376  			Expect(err).ToNot(HaveOccurred())
   377  		})
   378  
   379  		AfterEach(func() {
   380  			err := flag.Close()
   381  			Expect(err).ToNot(HaveOccurred())
   382  		})
   383  
   384  		It("It isn't quickly raised", func() {
   385  			time.Sleep(40 * time.Millisecond)
   386  			Expect(flag.Raised()).To(BeTrue())
   387  		})
   388  	})
   389  
   390  	When("Current holder closes the flag", func() {
   391  		It("Is raised by another process", func() {
   392  			var err error
   393  
   394  			// Create the first process:
   395  			first, err := NewFlag().
   396  				Logger(logger).
   397  				Handle(dbHandle).
   398  				Name("my_flag").
   399  				Process("first_process").
   400  				Interval(200 * time.Millisecond).
   401  				Jitter(0).
   402  				Build(ctx)
   403  			Expect(err).ToNot(HaveOccurred())
   404  
   405  			// Give the first process some time to get hold of the flag and then check
   406  			// that it did:
   407  			time.Sleep(200 * time.Millisecond)
   408  			Expect(first.Raised()).To(BeTrue())
   409  
   410  			// Create the second process:
   411  			second, err := NewFlag().
   412  				Logger(logger).
   413  				Handle(dbHandle).
   414  				Name("my_flag").
   415  				Process("second_process").
   416  				Interval(200 * time.Millisecond).
   417  				Jitter(0).
   418  				Build(ctx)
   419  			Expect(err).ToNot(HaveOccurred())
   420  			defer func() {
   421  				err = second.Close()
   422  				Expect(err).ToNot(HaveOccurred())
   423  			}()
   424  
   425  			// Give the second process some time to try to get hold of the flag and
   426  			// check that it didn't:
   427  			time.Sleep(200 * time.Millisecond)
   428  			Expect(second.Raised()).To(BeFalse())
   429  
   430  			// Close the first process so that it will fail to renew the flag:
   431  			err = first.Close()
   432  			Expect(err).ToNot(HaveOccurred())
   433  
   434  			// Allow time for the second process to get hold of the flag and check that
   435  			// it did:
   436  			time.Sleep(400 * time.Millisecond)
   437  			Expect(second.Raised()).To(BeTrue())
   438  		})
   439  	})
   440  
   441  	When("Current holder loses database connection", func() {
   442  		It("Is raised by another process", func() {
   443  			var err error
   444  
   445  			// Create the first process, but using a separate database handle, so that
   446  			// we can close it without affecting the second process:
   447  			altHandle := dbObject.MakeHandle()
   448  			first, err := NewFlag().
   449  				Logger(logger).
   450  				Handle(altHandle).
   451  				Name("my_flag").
   452  				Process("first_process").
   453  				Interval(200 * time.Millisecond).
   454  				Jitter(0).
   455  				Build(ctx)
   456  			Expect(err).ToNot(HaveOccurred())
   457  			defer func() {
   458  				err = first.Close()
   459  				Expect(err).ToNot(HaveOccurred())
   460  			}()
   461  
   462  			// Give the first process some time to get hold of the flag and then check
   463  			// that it did:
   464  			time.Sleep(200 * time.Millisecond)
   465  			Expect(first.Raised()).To(BeTrue())
   466  
   467  			// Create the second process:
   468  			second, err := NewFlag().
   469  				Logger(logger).
   470  				Handle(dbHandle).
   471  				Name("my_flag").
   472  				Process("second_process").
   473  				Interval(200 * time.Millisecond).
   474  				Jitter(0).
   475  				Build(ctx)
   476  			Expect(err).ToNot(HaveOccurred())
   477  			defer func() {
   478  				err = second.Close()
   479  				Expect(err).ToNot(HaveOccurred())
   480  			}()
   481  
   482  			// Give the second process some time to try to get hold of the flag and then
   483  			// check that it didn't:
   484  			time.Sleep(200 * time.Millisecond)
   485  			Expect(second.Raised()).To(BeFalse())
   486  
   487  			// Close the database connection of the first process:
   488  			err = altHandle.Close()
   489  			Expect(err).ToNot(HaveOccurred())
   490  
   491  			// Allow time for the second process to get hold of the flag and then check
   492  			// that it did:
   493  			time.Sleep(400 * time.Millisecond)
   494  			Expect(second.Raised()).To(BeTrue())
   495  		})
   496  	})
   497  
   498  	When("Stolen", func() {
   499  		It("Is lowers", func() {
   500  			var err error
   501  
   502  			// Create the flag:
   503  			flag, err := NewFlag().
   504  				Logger(logger).
   505  				Handle(dbHandle).
   506  				Name("my_flag").
   507  				Process("my_process").
   508  				Interval(200 * time.Millisecond).
   509  				Jitter(0).
   510  				Build(ctx)
   511  			Expect(err).ToNot(HaveOccurred())
   512  			defer func() {
   513  				err = flag.Close()
   514  				Expect(err).ToNot(HaveOccurred())
   515  			}()
   516  
   517  			// Give the process some time to get hold of the flag and check that it did:
   518  			time.Sleep(200 * time.Millisecond)
   519  			Expect(flag.Raised()).To(BeTrue())
   520  
   521  			// Steal the flag updating the database directly:
   522  			_, err = dbHandle.Exec(`
   523  				update
   524  					leadership_flags
   525  				set
   526  					holder = 'your_process',
   527  					version = version + 1,
   528  					timestamp = now()
   529  				where
   530  					name = 'my_flag'
   531  			`)
   532  			Expect(err).ToNot(HaveOccurred())
   533  
   534  			// Give the process some time to detect the situation and then check that it
   535  			// lowers the flag:
   536  			time.Sleep(200 * time.Millisecond)
   537  			Expect(flag.Raised()).To(BeFalse())
   538  		})
   539  	})
   540  
   541  	When("Stolen and then expired", func() {
   542  		It("Is recovers and raises", func() {
   543  			var err error
   544  
   545  			// Create the flag:
   546  			flag, err := NewFlag().
   547  				Logger(logger).
   548  				Handle(dbHandle).
   549  				Name("my_flag").
   550  				Process("my_process").
   551  				Interval(200 * time.Millisecond).
   552  				Jitter(0).
   553  				Build(ctx)
   554  			Expect(err).ToNot(HaveOccurred())
   555  			defer func() {
   556  				err = flag.Close()
   557  				Expect(err).ToNot(HaveOccurred())
   558  			}()
   559  
   560  			// Give the process some time to get hold of the flag and then check that
   561  			// it did:
   562  			time.Sleep(200 * time.Millisecond)
   563  			Expect(flag.Raised()).To(BeTrue())
   564  
   565  			// Force another holder updating the database directly:
   566  			_, err = dbHandle.Exec(`
   567  				update
   568  					leadership_flags
   569  				set
   570  					holder = 'your_process',
   571  					version = version + 1,
   572  					timestamp = now()
   573  				where
   574  					name = 'my_flag'
   575  			`)
   576  			Expect(err).ToNot(HaveOccurred())
   577  
   578  			// Give the process some time to detect the situation and check that it
   579  			// lowers the flag:
   580  			time.Sleep(200 * time.Millisecond)
   581  			Expect(flag.Raised()).To(BeFalse())
   582  
   583  			// Give it more time, so that the forced holder fails to renew and then check
   584  			// that it recovers and raises it:
   585  			time.Sleep(200 * time.Millisecond)
   586  			Expect(flag.Raised()).To(BeTrue())
   587  		})
   588  	})
   589  
   590  	// These sets of tests are two contesting leadership
   591  	When("Precheck is defined", func() {
   592  		var (
   593  			flip      *Flag
   594  			flop      *Flag
   595  			condition int32
   596  		)
   597  		const (
   598  			flagName    = "my_flag"
   599  			flipProcess = "flip" // This process will have the flag when the precheck condition is true.
   600  			flopProcess = "flop" // This process will have the flag when the precheck condition is false.
   601  			intervalMs  = 100
   602  		)
   603  
   604  		BeforeEach(func() {
   605  			var err error
   606  
   607  			// Create the database table:
   608  			CreateTable()
   609  
   610  			// Create the database row so that the flag is already held by another
   611  			// process that will eventually fail to update it:
   612  			_, err = dbHandle.Exec(fmt.Sprintf(`
   613  				insert into leadership_flags (
   614  					name,
   615  					holder,
   616  					version,
   617  					timestamp
   618  				) values (
   619  					'%s',
   620  					'your_process',
   621  					123,
   622  					now()
   623  				)
   624  			`, flagName))
   625  			Expect(err).ToNot(HaveOccurred())
   626  
   627  			// Create the object:
   628  			flip, err = NewFlag().
   629  				Logger(logger).
   630  				Handle(dbHandle).
   631  				Name(flagName).
   632  				Process(flipProcess).
   633  				PrecheckFunc(func() (bool, error) { return atomic.LoadInt32(&condition) != 1, nil }).
   634  				Interval(intervalMs * time.Millisecond).
   635  				Jitter(0).
   636  				Build(ctx)
   637  			Expect(err).ToNot(HaveOccurred())
   638  
   639  			flop, err = NewFlag().
   640  				Logger(logger).
   641  				Handle(dbHandle).
   642  				Name(flagName).
   643  				Process(flopProcess).
   644  				PrecheckFunc(func() (bool, error) { return atomic.LoadInt32(&condition) == 1, nil }).
   645  				Interval(intervalMs * time.Millisecond).
   646  				Jitter(0).
   647  				Build(ctx)
   648  			Expect(err).ToNot(HaveOccurred())
   649  		})
   650  
   651  		AfterEach(func() {
   652  			err := flip.Close()
   653  			Expect(err).ToNot(HaveOccurred())
   654  
   655  			err = flop.Close()
   656  			Expect(err).ToNot(HaveOccurred())
   657  		})
   658  
   659  		It("It is attained by the process with the precheck set to true", func() {
   660  			time.Sleep(2 * intervalMs * time.Millisecond)
   661  			Expect(flip.Raised()).To(BeTrue(), "Flip is expected to have the flag")
   662  			Expect(flop.Raised()).To(BeFalse(), "Flop is expected to not have the flag")
   663  
   664  			By("swapping the precheck condition")
   665  			atomic.SwapInt32(&condition, 1)
   666  
   667  			time.Sleep(4 * intervalMs * time.Millisecond)
   668  			Expect(flip.Raised()).To(BeFalse(), "Flip is expected to not have the flag after switch")
   669  			Expect(flop.Raised()).To(BeTrue(), "Flop is expected to have the flag after switch")
   670  		})
   671  	})
   672  })
   673  
   674  var _ = Describe("Flag metrics enabled", func() {
   675  	var ctx context.Context
   676  	var dbObject *Database
   677  	var dbHandle *sql.DB
   678  	var metricsServer *MetricsServer
   679  
   680  	BeforeEach(func() {
   681  		// Create a context:
   682  		ctx = context.Background()
   683  
   684  		// Create a database:
   685  		dbObject = dbServer.MakeDatabase()
   686  		dbHandle = dbObject.MakeHandle()
   687  
   688  		// Create the metrics server:
   689  		metricsServer = NewMetricsServer()
   690  	})
   691  
   692  	AfterEach(func() {
   693  		// Delete the database:
   694  		dbObject.Close()
   695  
   696  		// Stop the metrics server:
   697  		metricsServer.Close()
   698  	})
   699  
   700  	It("Generates state metrics", func() {
   701  		// Create the first process:
   702  		first, err := NewFlag().
   703  			Logger(logger).
   704  			Handle(dbHandle).
   705  			Name("my_flag").
   706  			Process("first_process").
   707  			Interval(200 * time.Millisecond).
   708  			Jitter(0).
   709  			MetricsSubsystem("my").
   710  			MetricsRegisterer(metricsServer.Registry()).
   711  			Build(ctx)
   712  		Expect(err).ToNot(HaveOccurred())
   713  		defer func() {
   714  			err = first.Close()
   715  			Expect(err).ToNot(HaveOccurred())
   716  		}()
   717  
   718  		// Give it time to raise:
   719  		time.Sleep(200 * time.Millisecond)
   720  		Expect(first.Raised()).To(BeTrue())
   721  
   722  		// Create the second process:
   723  		second, err := NewFlag().
   724  			Logger(logger).
   725  			Handle(dbHandle).
   726  			Name("my_flag").
   727  			Process("second_process").
   728  			Interval(200 * time.Millisecond).
   729  			Jitter(0).
   730  			MetricsSubsystem("my").
   731  			MetricsRegisterer(metricsServer.Registry()).
   732  			Build(ctx)
   733  		Expect(err).ToNot(HaveOccurred())
   734  		defer func() {
   735  			err = second.Close()
   736  			Expect(err).ToNot(HaveOccurred())
   737  		}()
   738  
   739  		// Git it time to lower:
   740  		time.Sleep(200 * time.Millisecond)
   741  		Expect(second.Raised()).To(BeFalse())
   742  
   743  		// Verify the metrics:
   744  		metrics := metricsServer.Metrics()
   745  		Expect(metrics).To(MatchLine(`^my_leadership_flag_state\{name="my_flag",process="first_process"\} 1$`))
   746  		Expect(metrics).To(MatchLine(`^my_leadership_flag_state\{name="my_flag",process="second_process"\} 0$`))
   747  	})
   748  })