github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/orderer/common/localconfig/config.go (about)

     1  // Copyright IBM Corp. All Rights Reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package localconfig
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"path/filepath"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/Shopify/sarama"
    15  	bccsp "github.com/osdi23p228/fabric/bccsp/factory"
    16  	"github.com/osdi23p228/fabric/common/flogging"
    17  	"github.com/osdi23p228/fabric/common/viperutil"
    18  	coreconfig "github.com/osdi23p228/fabric/core/config"
    19  	"github.com/spf13/viper"
    20  )
    21  
    22  // Prefix for environment variables.
    23  const Prefix = "ORDERER"
    24  
    25  var logger = flogging.MustGetLogger("localconfig")
    26  
    27  // TopLevel directly corresponds to the orderer config YAML.
    28  // Note, for non 1-1 mappings, you may append
    29  // something like `mapstructure:"weirdFoRMat"` to
    30  // modify the default mapping, see the "Unmarshal"
    31  // section of https://github.com/spf13/viper for more info.
    32  type TopLevel struct {
    33  	General              General
    34  	FileLedger           FileLedger
    35  	Kafka                Kafka
    36  	Debug                Debug
    37  	Consensus            interface{}
    38  	Operations           Operations
    39  	Metrics              Metrics
    40  	ChannelParticipation ChannelParticipation
    41  }
    42  
    43  // General contains config which should be common among all orderer types.
    44  type General struct {
    45  	ListenAddress     string
    46  	ListenPort        uint16
    47  	TLS               TLS
    48  	Cluster           Cluster
    49  	Keepalive         Keepalive
    50  	ConnectionTimeout time.Duration
    51  	GenesisMethod     string // For compatibility only, will be replaced by BootstrapMethod
    52  	GenesisFile       string // For compatibility only, will be replaced by BootstrapFile
    53  	BootstrapMethod   string
    54  	BootstrapFile     string
    55  	Profile           Profile
    56  	LocalMSPDir       string
    57  	LocalMSPID        string
    58  	BCCSP             *bccsp.FactoryOpts
    59  	Authentication    Authentication
    60  }
    61  
    62  type Cluster struct {
    63  	ListenAddress                        string
    64  	ListenPort                           uint16
    65  	ServerCertificate                    string
    66  	ServerPrivateKey                     string
    67  	ClientCertificate                    string
    68  	ClientPrivateKey                     string
    69  	RootCAs                              []string
    70  	DialTimeout                          time.Duration
    71  	RPCTimeout                           time.Duration
    72  	ReplicationBufferSize                int
    73  	ReplicationPullTimeout               time.Duration
    74  	ReplicationRetryTimeout              time.Duration
    75  	ReplicationBackgroundRefreshInterval time.Duration
    76  	ReplicationMaxRetries                int
    77  	SendBufferSize                       int
    78  	CertExpirationWarningThreshold       time.Duration
    79  	TLSHandshakeTimeShift                time.Duration
    80  }
    81  
    82  // Keepalive contains configuration for gRPC servers.
    83  type Keepalive struct {
    84  	ServerMinInterval time.Duration
    85  	ServerInterval    time.Duration
    86  	ServerTimeout     time.Duration
    87  }
    88  
    89  // TLS contains configuration for TLS connections.
    90  type TLS struct {
    91  	Enabled               bool
    92  	PrivateKey            string
    93  	Certificate           string
    94  	RootCAs               []string
    95  	ClientAuthRequired    bool
    96  	ClientRootCAs         []string
    97  	TLSHandshakeTimeShift time.Duration
    98  }
    99  
   100  // SASLPlain contains configuration for SASL/PLAIN authentication
   101  type SASLPlain struct {
   102  	Enabled  bool
   103  	User     string
   104  	Password string
   105  }
   106  
   107  // Authentication contains configuration parameters related to authenticating
   108  // client messages.
   109  type Authentication struct {
   110  	TimeWindow         time.Duration
   111  	NoExpirationChecks bool
   112  }
   113  
   114  // Profile contains configuration for Go pprof profiling.
   115  type Profile struct {
   116  	Enabled bool
   117  	Address string
   118  }
   119  
   120  // FileLedger contains configuration for the file-based ledger.
   121  type FileLedger struct {
   122  	Location string
   123  	Prefix   string
   124  }
   125  
   126  // Kafka contains configuration for the Kafka-based orderer.
   127  type Kafka struct {
   128  	Retry     Retry
   129  	Verbose   bool
   130  	Version   sarama.KafkaVersion // TODO Move this to global config
   131  	TLS       TLS
   132  	SASLPlain SASLPlain
   133  	Topic     Topic
   134  }
   135  
   136  // Retry contains configuration related to retries and timeouts when the
   137  // connection to the Kafka cluster cannot be established, or when Metadata
   138  // requests needs to be repeated (because the cluster is in the middle of a
   139  // leader election).
   140  type Retry struct {
   141  	ShortInterval   time.Duration
   142  	ShortTotal      time.Duration
   143  	LongInterval    time.Duration
   144  	LongTotal       time.Duration
   145  	NetworkTimeouts NetworkTimeouts
   146  	Metadata        Metadata
   147  	Producer        Producer
   148  	Consumer        Consumer
   149  }
   150  
   151  // NetworkTimeouts contains the socket timeouts for network requests to the
   152  // Kafka cluster.
   153  type NetworkTimeouts struct {
   154  	DialTimeout  time.Duration
   155  	ReadTimeout  time.Duration
   156  	WriteTimeout time.Duration
   157  }
   158  
   159  // Metadata contains configuration for the metadata requests to the Kafka
   160  // cluster.
   161  type Metadata struct {
   162  	RetryMax     int
   163  	RetryBackoff time.Duration
   164  }
   165  
   166  // Producer contains configuration for the producer's retries when failing to
   167  // post a message to a Kafka partition.
   168  type Producer struct {
   169  	RetryMax     int
   170  	RetryBackoff time.Duration
   171  }
   172  
   173  // Consumer contains configuration for the consumer's retries when failing to
   174  // read from a Kafa partition.
   175  type Consumer struct {
   176  	RetryBackoff time.Duration
   177  }
   178  
   179  // Topic contains the settings to use when creating Kafka topics
   180  type Topic struct {
   181  	ReplicationFactor int16
   182  }
   183  
   184  // Debug contains configuration for the orderer's debug parameters.
   185  type Debug struct {
   186  	BroadcastTraceDir string
   187  	DeliverTraceDir   string
   188  }
   189  
   190  // Operations configures the operations endpoint for the orderer.
   191  type Operations struct {
   192  	ListenAddress string
   193  	TLS           TLS
   194  }
   195  
   196  // Metrics configures the metrics provider for the orderer.
   197  type Metrics struct {
   198  	Provider string
   199  	Statsd   Statsd
   200  }
   201  
   202  // Statsd provides the configuration required to emit statsd metrics from the orderer.
   203  type Statsd struct {
   204  	Network       string
   205  	Address       string
   206  	WriteInterval time.Duration
   207  	Prefix        string
   208  }
   209  
   210  // ChannelParticipation provides the channel participation API configuration for the orderer.
   211  // Channel participation uses the same ListenAddress and TLS settings of the Operations service.
   212  type ChannelParticipation struct {
   213  	Enabled       bool
   214  	RemoveStorage bool // Whether to permanently remove storage on channel removal.
   215  }
   216  
   217  // Defaults carries the default orderer configuration values.
   218  var Defaults = TopLevel{
   219  	General: General{
   220  		ListenAddress:   "127.0.0.1",
   221  		ListenPort:      7050,
   222  		BootstrapMethod: "file",
   223  		BootstrapFile:   "genesisblock",
   224  		Profile: Profile{
   225  			Enabled: false,
   226  			Address: "0.0.0.0:6060",
   227  		},
   228  		Cluster: Cluster{
   229  			ReplicationMaxRetries:                12,
   230  			RPCTimeout:                           time.Second * 7,
   231  			DialTimeout:                          time.Second * 5,
   232  			ReplicationBufferSize:                20971520,
   233  			SendBufferSize:                       10,
   234  			ReplicationBackgroundRefreshInterval: time.Minute * 5,
   235  			ReplicationRetryTimeout:              time.Second * 5,
   236  			ReplicationPullTimeout:               time.Second * 5,
   237  			CertExpirationWarningThreshold:       time.Hour * 24 * 7,
   238  		},
   239  		LocalMSPDir: "msp",
   240  		LocalMSPID:  "SampleOrg",
   241  		BCCSP:       bccsp.GetDefaultOpts(),
   242  		Authentication: Authentication{
   243  			TimeWindow: time.Duration(15 * time.Minute),
   244  		},
   245  	},
   246  	FileLedger: FileLedger{
   247  		Location: "/var/hyperledger/production/orderer",
   248  		Prefix:   "hyperledger-fabric-ordererledger",
   249  	},
   250  	Kafka: Kafka{
   251  		Retry: Retry{
   252  			ShortInterval: 1 * time.Minute,
   253  			ShortTotal:    10 * time.Minute,
   254  			LongInterval:  10 * time.Minute,
   255  			LongTotal:     12 * time.Hour,
   256  			NetworkTimeouts: NetworkTimeouts{
   257  				DialTimeout:  30 * time.Second,
   258  				ReadTimeout:  30 * time.Second,
   259  				WriteTimeout: 30 * time.Second,
   260  			},
   261  			Metadata: Metadata{
   262  				RetryBackoff: 250 * time.Millisecond,
   263  				RetryMax:     3,
   264  			},
   265  			Producer: Producer{
   266  				RetryBackoff: 100 * time.Millisecond,
   267  				RetryMax:     3,
   268  			},
   269  			Consumer: Consumer{
   270  				RetryBackoff: 2 * time.Second,
   271  			},
   272  		},
   273  		Verbose: false,
   274  		Version: sarama.V0_10_2_0,
   275  		TLS: TLS{
   276  			Enabled: false,
   277  		},
   278  		Topic: Topic{
   279  			ReplicationFactor: 3,
   280  		},
   281  	},
   282  	Debug: Debug{
   283  		BroadcastTraceDir: "",
   284  		DeliverTraceDir:   "",
   285  	},
   286  	Operations: Operations{
   287  		ListenAddress: "127.0.0.1:0",
   288  	},
   289  	Metrics: Metrics{
   290  		Provider: "disabled",
   291  	},
   292  	ChannelParticipation: ChannelParticipation{
   293  		Enabled:       false,
   294  		RemoveStorage: false,
   295  	},
   296  }
   297  
   298  // Load parses the orderer YAML file and environment, producing
   299  // a struct suitable for config use, returning error on failure.
   300  func Load() (*TopLevel, error) {
   301  	return cache.load()
   302  }
   303  
   304  // configCache stores marshalled bytes of config structures that produced from
   305  // EnhancedExactUnmarshal. Cache key is the path of the configuration file that was used.
   306  type configCache struct {
   307  	mutex sync.Mutex
   308  	cache map[string][]byte
   309  }
   310  
   311  var cache = &configCache{}
   312  
   313  // Load will load the configuration and cache it on the first call; subsequent
   314  // calls will return a clone of the configuration that was previously loaded.
   315  func (c *configCache) load() (*TopLevel, error) {
   316  	var uconf TopLevel
   317  
   318  	config := viper.New()
   319  	coreconfig.InitViper(config, "orderer")
   320  	config.SetEnvPrefix(Prefix)
   321  	config.AutomaticEnv()
   322  	replacer := strings.NewReplacer(".", "_")
   323  	config.SetEnvKeyReplacer(replacer)
   324  
   325  	if err := config.ReadInConfig(); err != nil {
   326  		return nil, fmt.Errorf("Error reading configuration: %s", err)
   327  	}
   328  
   329  	c.mutex.Lock()
   330  	defer c.mutex.Unlock()
   331  	serializedConf, ok := c.cache[config.ConfigFileUsed()]
   332  	if !ok {
   333  		err := viperutil.EnhancedExactUnmarshal(config, &uconf)
   334  		if err != nil {
   335  			return nil, fmt.Errorf("Error unmarshaling config into struct: %s", err)
   336  		}
   337  
   338  		serializedConf, err = json.Marshal(uconf)
   339  		if err != nil {
   340  			return nil, err
   341  		}
   342  
   343  		if c.cache == nil {
   344  			c.cache = map[string][]byte{}
   345  		}
   346  		c.cache[config.ConfigFileUsed()] = serializedConf
   347  	}
   348  
   349  	err := json.Unmarshal(serializedConf, &uconf)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	uconf.completeInitialization(filepath.Dir(config.ConfigFileUsed()))
   354  
   355  	return &uconf, nil
   356  }
   357  
   358  func (c *TopLevel) completeInitialization(configDir string) {
   359  	defer func() {
   360  		// Translate any paths for cluster TLS configuration if applicable
   361  		if c.General.Cluster.ClientPrivateKey != "" {
   362  			coreconfig.TranslatePathInPlace(configDir, &c.General.Cluster.ClientPrivateKey)
   363  		}
   364  		if c.General.Cluster.ClientCertificate != "" {
   365  			coreconfig.TranslatePathInPlace(configDir, &c.General.Cluster.ClientCertificate)
   366  		}
   367  		c.General.Cluster.RootCAs = translateCAs(configDir, c.General.Cluster.RootCAs)
   368  		// Translate any paths for general TLS configuration
   369  		c.General.TLS.RootCAs = translateCAs(configDir, c.General.TLS.RootCAs)
   370  		c.General.TLS.ClientRootCAs = translateCAs(configDir, c.General.TLS.ClientRootCAs)
   371  		coreconfig.TranslatePathInPlace(configDir, &c.General.TLS.PrivateKey)
   372  		coreconfig.TranslatePathInPlace(configDir, &c.General.TLS.Certificate)
   373  		coreconfig.TranslatePathInPlace(configDir, &c.General.BootstrapFile)
   374  		coreconfig.TranslatePathInPlace(configDir, &c.General.LocalMSPDir)
   375  		// Translate file ledger location
   376  		coreconfig.TranslatePathInPlace(configDir, &c.FileLedger.Location)
   377  	}()
   378  
   379  	for {
   380  		switch {
   381  		case c.General.ListenAddress == "":
   382  			logger.Infof("General.ListenAddress unset, setting to %s", Defaults.General.ListenAddress)
   383  			c.General.ListenAddress = Defaults.General.ListenAddress
   384  		case c.General.ListenPort == 0:
   385  			logger.Infof("General.ListenPort unset, setting to %v", Defaults.General.ListenPort)
   386  			c.General.ListenPort = Defaults.General.ListenPort
   387  		case c.General.BootstrapMethod == "":
   388  			if c.General.GenesisMethod != "" {
   389  				// This is to keep the compatibility with old config file that uses genesismethod
   390  				logger.Warn("General.GenesisMethod should be replaced by General.BootstrapMethod")
   391  				c.General.BootstrapMethod = c.General.GenesisMethod
   392  			} else {
   393  				c.General.BootstrapMethod = Defaults.General.BootstrapMethod
   394  			}
   395  		case c.General.BootstrapFile == "":
   396  			if c.General.GenesisFile != "" {
   397  				// This is to keep the compatibility with old config file that uses genesisfile
   398  				logger.Warn("General.GenesisFile should be replaced by General.BootstrapFile")
   399  				c.General.BootstrapFile = c.General.GenesisFile
   400  			} else {
   401  				c.General.BootstrapFile = Defaults.General.BootstrapFile
   402  			}
   403  		case c.General.Cluster.RPCTimeout == 0:
   404  			c.General.Cluster.RPCTimeout = Defaults.General.Cluster.RPCTimeout
   405  		case c.General.Cluster.DialTimeout == 0:
   406  			c.General.Cluster.DialTimeout = Defaults.General.Cluster.DialTimeout
   407  		case c.General.Cluster.ReplicationMaxRetries == 0:
   408  			c.General.Cluster.ReplicationMaxRetries = Defaults.General.Cluster.ReplicationMaxRetries
   409  		case c.General.Cluster.SendBufferSize == 0:
   410  			c.General.Cluster.SendBufferSize = Defaults.General.Cluster.SendBufferSize
   411  		case c.General.Cluster.ReplicationBufferSize == 0:
   412  			c.General.Cluster.ReplicationBufferSize = Defaults.General.Cluster.ReplicationBufferSize
   413  		case c.General.Cluster.ReplicationPullTimeout == 0:
   414  			c.General.Cluster.ReplicationPullTimeout = Defaults.General.Cluster.ReplicationPullTimeout
   415  		case c.General.Cluster.ReplicationRetryTimeout == 0:
   416  			c.General.Cluster.ReplicationRetryTimeout = Defaults.General.Cluster.ReplicationRetryTimeout
   417  		case c.General.Cluster.ReplicationBackgroundRefreshInterval == 0:
   418  			c.General.Cluster.ReplicationBackgroundRefreshInterval = Defaults.General.Cluster.ReplicationBackgroundRefreshInterval
   419  		case c.General.Cluster.CertExpirationWarningThreshold == 0:
   420  			c.General.Cluster.CertExpirationWarningThreshold = Defaults.General.Cluster.CertExpirationWarningThreshold
   421  		case c.Kafka.TLS.Enabled && c.Kafka.TLS.Certificate == "":
   422  			logger.Panicf("General.Kafka.TLS.Certificate must be set if General.Kafka.TLS.Enabled is set to true.")
   423  		case c.Kafka.TLS.Enabled && c.Kafka.TLS.PrivateKey == "":
   424  			logger.Panicf("General.Kafka.TLS.PrivateKey must be set if General.Kafka.TLS.Enabled is set to true.")
   425  		case c.Kafka.TLS.Enabled && c.Kafka.TLS.RootCAs == nil:
   426  			logger.Panicf("General.Kafka.TLS.CertificatePool must be set if General.Kafka.TLS.Enabled is set to true.")
   427  
   428  		case c.Kafka.SASLPlain.Enabled && c.Kafka.SASLPlain.User == "":
   429  			logger.Panic("General.Kafka.SASLPlain.User must be set if General.Kafka.SASLPlain.Enabled is set to true.")
   430  		case c.Kafka.SASLPlain.Enabled && c.Kafka.SASLPlain.Password == "":
   431  			logger.Panic("General.Kafka.SASLPlain.Password must be set if General.Kafka.SASLPlain.Enabled is set to true.")
   432  
   433  		case c.General.Profile.Enabled && c.General.Profile.Address == "":
   434  			logger.Infof("Profiling enabled and General.Profile.Address unset, setting to %s", Defaults.General.Profile.Address)
   435  			c.General.Profile.Address = Defaults.General.Profile.Address
   436  
   437  		case c.General.LocalMSPDir == "":
   438  			logger.Infof("General.LocalMSPDir unset, setting to %s", Defaults.General.LocalMSPDir)
   439  			c.General.LocalMSPDir = Defaults.General.LocalMSPDir
   440  		case c.General.LocalMSPID == "":
   441  			logger.Infof("General.LocalMSPID unset, setting to %s", Defaults.General.LocalMSPID)
   442  			c.General.LocalMSPID = Defaults.General.LocalMSPID
   443  
   444  		case c.General.Authentication.TimeWindow == 0:
   445  			logger.Infof("General.Authentication.TimeWindow unset, setting to %s", Defaults.General.Authentication.TimeWindow)
   446  			c.General.Authentication.TimeWindow = Defaults.General.Authentication.TimeWindow
   447  
   448  		case c.FileLedger.Prefix == "":
   449  			logger.Infof("FileLedger.Prefix unset, setting to %s", Defaults.FileLedger.Prefix)
   450  			c.FileLedger.Prefix = Defaults.FileLedger.Prefix
   451  
   452  		case c.Kafka.Retry.ShortInterval == 0:
   453  			logger.Infof("Kafka.Retry.ShortInterval unset, setting to %v", Defaults.Kafka.Retry.ShortInterval)
   454  			c.Kafka.Retry.ShortInterval = Defaults.Kafka.Retry.ShortInterval
   455  		case c.Kafka.Retry.ShortTotal == 0:
   456  			logger.Infof("Kafka.Retry.ShortTotal unset, setting to %v", Defaults.Kafka.Retry.ShortTotal)
   457  			c.Kafka.Retry.ShortTotal = Defaults.Kafka.Retry.ShortTotal
   458  		case c.Kafka.Retry.LongInterval == 0:
   459  			logger.Infof("Kafka.Retry.LongInterval unset, setting to %v", Defaults.Kafka.Retry.LongInterval)
   460  			c.Kafka.Retry.LongInterval = Defaults.Kafka.Retry.LongInterval
   461  		case c.Kafka.Retry.LongTotal == 0:
   462  			logger.Infof("Kafka.Retry.LongTotal unset, setting to %v", Defaults.Kafka.Retry.LongTotal)
   463  			c.Kafka.Retry.LongTotal = Defaults.Kafka.Retry.LongTotal
   464  
   465  		case c.Kafka.Retry.NetworkTimeouts.DialTimeout == 0:
   466  			logger.Infof("Kafka.Retry.NetworkTimeouts.DialTimeout unset, setting to %v", Defaults.Kafka.Retry.NetworkTimeouts.DialTimeout)
   467  			c.Kafka.Retry.NetworkTimeouts.DialTimeout = Defaults.Kafka.Retry.NetworkTimeouts.DialTimeout
   468  		case c.Kafka.Retry.NetworkTimeouts.ReadTimeout == 0:
   469  			logger.Infof("Kafka.Retry.NetworkTimeouts.ReadTimeout unset, setting to %v", Defaults.Kafka.Retry.NetworkTimeouts.ReadTimeout)
   470  			c.Kafka.Retry.NetworkTimeouts.ReadTimeout = Defaults.Kafka.Retry.NetworkTimeouts.ReadTimeout
   471  		case c.Kafka.Retry.NetworkTimeouts.WriteTimeout == 0:
   472  			logger.Infof("Kafka.Retry.NetworkTimeouts.WriteTimeout unset, setting to %v", Defaults.Kafka.Retry.NetworkTimeouts.WriteTimeout)
   473  			c.Kafka.Retry.NetworkTimeouts.WriteTimeout = Defaults.Kafka.Retry.NetworkTimeouts.WriteTimeout
   474  
   475  		case c.Kafka.Retry.Metadata.RetryBackoff == 0:
   476  			logger.Infof("Kafka.Retry.Metadata.RetryBackoff unset, setting to %v", Defaults.Kafka.Retry.Metadata.RetryBackoff)
   477  			c.Kafka.Retry.Metadata.RetryBackoff = Defaults.Kafka.Retry.Metadata.RetryBackoff
   478  		case c.Kafka.Retry.Metadata.RetryMax == 0:
   479  			logger.Infof("Kafka.Retry.Metadata.RetryMax unset, setting to %v", Defaults.Kafka.Retry.Metadata.RetryMax)
   480  			c.Kafka.Retry.Metadata.RetryMax = Defaults.Kafka.Retry.Metadata.RetryMax
   481  
   482  		case c.Kafka.Retry.Producer.RetryBackoff == 0:
   483  			logger.Infof("Kafka.Retry.Producer.RetryBackoff unset, setting to %v", Defaults.Kafka.Retry.Producer.RetryBackoff)
   484  			c.Kafka.Retry.Producer.RetryBackoff = Defaults.Kafka.Retry.Producer.RetryBackoff
   485  		case c.Kafka.Retry.Producer.RetryMax == 0:
   486  			logger.Infof("Kafka.Retry.Producer.RetryMax unset, setting to %v", Defaults.Kafka.Retry.Producer.RetryMax)
   487  			c.Kafka.Retry.Producer.RetryMax = Defaults.Kafka.Retry.Producer.RetryMax
   488  
   489  		case c.Kafka.Retry.Consumer.RetryBackoff == 0:
   490  			logger.Infof("Kafka.Retry.Consumer.RetryBackoff unset, setting to %v", Defaults.Kafka.Retry.Consumer.RetryBackoff)
   491  			c.Kafka.Retry.Consumer.RetryBackoff = Defaults.Kafka.Retry.Consumer.RetryBackoff
   492  
   493  		case c.Kafka.Version == sarama.KafkaVersion{}:
   494  			logger.Infof("Kafka.Version unset, setting to %v", Defaults.Kafka.Version)
   495  			c.Kafka.Version = Defaults.Kafka.Version
   496  
   497  		default:
   498  			return
   499  		}
   500  	}
   501  }
   502  
   503  func translateCAs(configDir string, certificateAuthorities []string) []string {
   504  	var results []string
   505  	for _, ca := range certificateAuthorities {
   506  		result := coreconfig.TranslatePath(configDir, ca)
   507  		results = append(results, result)
   508  	}
   509  	return results
   510  }