github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/sorted/mongo/mongokv.go (about)

     1  /*
     2  Copyright 2013 The Camlistore Authors.
     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 mongo provides an implementation of sorted.KeyValue
    18  // using MongoDB.
    19  package mongo
    20  
    21  import (
    22  	"bytes"
    23  	"errors"
    24  	"sync"
    25  	"time"
    26  
    27  	"camlistore.org/pkg/jsonconfig"
    28  	"camlistore.org/pkg/sorted"
    29  
    30  	"camlistore.org/third_party/labix.org/v2/mgo"
    31  	"camlistore.org/third_party/labix.org/v2/mgo/bson"
    32  )
    33  
    34  // We explicitely separate the key and the value in a document,
    35  // instead of simply storing as key:value, to avoid problems
    36  // such as "." being an illegal char in a key name. Also because
    37  // there is no way to do partial matching for key names (one can
    38  // only check for their existence with bson.M{$exists: true}).
    39  const (
    40  	CollectionName = "keys" // MongoDB collection, equiv. to SQL table
    41  	mgoKey         = "k"
    42  	mgoValue       = "v"
    43  )
    44  
    45  func init() {
    46  	sorted.RegisterKeyValue("mongo", newKeyValueFromJSONConfig)
    47  }
    48  
    49  // Config holds the parameters used to connect to MongoDB.
    50  type Config struct {
    51  	Server   string // Required. Defaults to "localhost" in ConfigFromJSON.
    52  	Database string // Required.
    53  	User     string // Optional, unless the server was configured with auth on.
    54  	Password string // Optional, unless the server was configured with auth on.
    55  }
    56  
    57  // ConfigFromJSON populates Config from cfg, and validates
    58  // cfg. It returns an error if cfg fails to validate.
    59  func ConfigFromJSON(cfg jsonconfig.Obj) (Config, error) {
    60  	conf := Config{
    61  		Server:   cfg.OptionalString("host", "localhost"),
    62  		Database: cfg.RequiredString("database"),
    63  		User:     cfg.OptionalString("user", ""),
    64  		Password: cfg.OptionalString("password", ""),
    65  	}
    66  	if err := cfg.Validate(); err != nil {
    67  		return Config{}, err
    68  	}
    69  	return conf, nil
    70  }
    71  
    72  // NewKeyValue returns a KeyValue implementation on top of MongoDB.
    73  func NewKeyValue(cfg Config) (sorted.KeyValue, error) {
    74  	ins := &instance{
    75  		conf: cfg,
    76  	}
    77  	db, err := ins.getCollection()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	return &keyValue{db: db, session: ins.session}, nil
    82  }
    83  
    84  func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) {
    85  	conf, err := ConfigFromJSON(cfg)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	return NewKeyValue(conf)
    90  }
    91  
    92  // Implementation of Iterator
    93  type iter struct {
    94  	res bson.M
    95  	*mgo.Iter
    96  	end []byte
    97  }
    98  
    99  func (it *iter) Next() bool {
   100  	if !it.Iter.Next(&it.res) {
   101  		return false
   102  	}
   103  	if len(it.end) > 0 && bytes.Compare(it.KeyBytes(), it.end) >= 0 {
   104  		return false
   105  	}
   106  	return true
   107  }
   108  
   109  func (it *iter) Key() string {
   110  	key, ok := (it.res[mgoKey]).(string)
   111  	if !ok {
   112  		return ""
   113  	}
   114  	return key
   115  }
   116  
   117  func (it *iter) KeyBytes() []byte {
   118  	// TODO(bradfitz,mpl): this is less efficient than the string way. we should
   119  	// do better here, somehow, like all the other KeyValue iterators.
   120  	// For now:
   121  	return []byte(it.Key())
   122  }
   123  
   124  func (it *iter) Value() string {
   125  	value, ok := (it.res[mgoValue]).(string)
   126  	if !ok {
   127  		return ""
   128  	}
   129  	return value
   130  }
   131  
   132  func (it *iter) ValueBytes() []byte {
   133  	// TODO(bradfitz,mpl): this is less efficient than the string way. we should
   134  	// do better here, somehow, like all the other KeyValue iterators.
   135  	// For now:
   136  	return []byte(it.Value())
   137  }
   138  
   139  func (it *iter) Close() error {
   140  	return it.Iter.Close()
   141  }
   142  
   143  // Implementation of KeyValue
   144  type keyValue struct {
   145  	session *mgo.Session // so we can close it
   146  	mu      sync.Mutex   // guards db
   147  	db      *mgo.Collection
   148  }
   149  
   150  func (kv *keyValue) Get(key string) (string, error) {
   151  	kv.mu.Lock()
   152  	defer kv.mu.Unlock()
   153  	res := bson.M{}
   154  	q := kv.db.Find(&bson.M{mgoKey: key})
   155  	err := q.One(&res)
   156  	if err != nil {
   157  		if err == mgo.ErrNotFound {
   158  			return "", sorted.ErrNotFound
   159  		} else {
   160  			return "", err
   161  		}
   162  	}
   163  	return res[mgoValue].(string), err
   164  }
   165  
   166  func (kv *keyValue) Find(start, end string) sorted.Iterator {
   167  	kv.mu.Lock()
   168  	defer kv.mu.Unlock()
   169  	it := kv.db.Find(&bson.M{mgoKey: &bson.M{"$gte": start}}).Sort(mgoKey).Iter()
   170  	return &iter{res: bson.M{}, Iter: it, end: []byte(end)}
   171  }
   172  
   173  func (kv *keyValue) Set(key, value string) error {
   174  	kv.mu.Lock()
   175  	defer kv.mu.Unlock()
   176  	_, err := kv.db.Upsert(&bson.M{mgoKey: key}, &bson.M{mgoKey: key, mgoValue: value})
   177  	return err
   178  }
   179  
   180  // Delete removes the document with the matching key.
   181  func (kv *keyValue) Delete(key string) error {
   182  	kv.mu.Lock()
   183  	defer kv.mu.Unlock()
   184  	err := kv.db.Remove(&bson.M{mgoKey: key})
   185  	if err == mgo.ErrNotFound {
   186  		return nil
   187  	}
   188  	return err
   189  }
   190  
   191  // Wipe removes all documents from the collection.
   192  func (kv *keyValue) Wipe() error {
   193  	kv.mu.Lock()
   194  	defer kv.mu.Unlock()
   195  	_, err := kv.db.RemoveAll(nil)
   196  	return err
   197  }
   198  
   199  type batch interface {
   200  	Mutations() []sorted.Mutation
   201  }
   202  
   203  func (kv *keyValue) BeginBatch() sorted.BatchMutation {
   204  	return sorted.NewBatchMutation()
   205  }
   206  
   207  func (kv *keyValue) CommitBatch(bm sorted.BatchMutation) error {
   208  	b, ok := bm.(batch)
   209  	if !ok {
   210  		return errors.New("invalid batch type")
   211  	}
   212  
   213  	kv.mu.Lock()
   214  	defer kv.mu.Unlock()
   215  	for _, m := range b.Mutations() {
   216  		if m.IsDelete() {
   217  			if err := kv.db.Remove(bson.M{mgoKey: m.Key()}); err != nil {
   218  				return err
   219  			}
   220  		} else {
   221  			if _, err := kv.db.Upsert(&bson.M{mgoKey: m.Key()}, &bson.M{mgoKey: m.Key(), mgoValue: m.Value()}); err != nil {
   222  				return err
   223  			}
   224  		}
   225  	}
   226  	return nil
   227  }
   228  
   229  func (kv *keyValue) Close() error {
   230  	kv.session.Close()
   231  	return nil
   232  }
   233  
   234  // Ping tests if MongoDB on host can be dialed.
   235  func Ping(host string, timeout time.Duration) bool {
   236  	return (&instance{conf: Config{Server: host}}).ping(timeout)
   237  }
   238  
   239  // instance helps with the low level details about
   240  // the connection to MongoDB.
   241  type instance struct {
   242  	conf    Config
   243  	session *mgo.Session
   244  }
   245  
   246  func (ins *instance) url() string {
   247  	if ins.conf.User == "" || ins.conf.Password == "" {
   248  		return ins.conf.Server
   249  	}
   250  	return ins.conf.User + ":" + ins.conf.Password + "@" + ins.conf.Server + "/" + ins.conf.Database
   251  }
   252  
   253  // ping won't work with old (1.2) mongo servers.
   254  func (ins *instance) ping(timeout time.Duration) bool {
   255  	session, err := mgo.DialWithTimeout(ins.url(), timeout)
   256  	if err != nil {
   257  		return false
   258  	}
   259  	defer session.Close()
   260  	session.SetSyncTimeout(timeout)
   261  	if err = session.Ping(); err != nil {
   262  		return false
   263  	}
   264  	return true
   265  }
   266  
   267  func (ins *instance) getConnection() (*mgo.Session, error) {
   268  	if ins.session != nil {
   269  		return ins.session, nil
   270  	}
   271  	// TODO(mpl): do some "client caching" as in mysql, to avoid systematically dialing?
   272  	session, err := mgo.Dial(ins.url())
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	session.SetMode(mgo.Monotonic, true)
   277  	session.SetSafe(&mgo.Safe{}) // so we get an ErrNotFound error when deleting an absent key
   278  	ins.session = session
   279  	return session, nil
   280  }
   281  
   282  // TODO(mpl): I'm only calling getCollection at the beginning, and
   283  // keeping the collection around and reusing it everywhere, instead
   284  // of calling getCollection everytime, because that's the easiest.
   285  // But I can easily change that. Gustavo says it does not make
   286  // much difference either way.
   287  // Brad, what do you think?
   288  func (ins *instance) getCollection() (*mgo.Collection, error) {
   289  	session, err := ins.getConnection()
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	session.SetSafe(&mgo.Safe{})
   294  	session.SetMode(mgo.Strong, true)
   295  	c := session.DB(ins.conf.Database).C(CollectionName)
   296  	return c, nil
   297  }