github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/mongo/prealloc.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package mongo 5 6 import ( 7 "bytes" 8 "fmt" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime" 13 "strconv" 14 "strings" 15 16 "github.com/juju/juju/juju/arch" 17 ) 18 19 const ( 20 // preallocAlign must divide all preallocated files' sizes. 21 preallocAlign = 4096 22 ) 23 24 var ( 25 runtimeGOOS = runtime.GOOS 26 hostWordSize = arch.Info[arch.HostArch()].WordSize 27 28 // zeroes is used by preallocFile to write zeroes to 29 // preallocated Mongo data files. 30 zeroes = make([]byte, 64*1024) 31 32 minOplogSizeMB = 1024 33 maxOplogSizeMB = 50 * 1024 34 35 availSpace = fsAvailSpace 36 preallocFile = doPreallocFile 37 ) 38 39 // preallocOplog preallocates the Mongo oplog in the 40 // specified Mongo datadabase directory. 41 func preallocOplog(dir string) error { 42 size, err := oplogSize(dir) 43 if err != nil { 44 return err 45 } 46 // oplogSize returns MB, we want to work in bytes. 47 sizes := preallocFileSizes(size * 1024 * 1024) 48 prefix := filepath.Join(dir, "local.") 49 return preallocFiles(prefix, sizes...) 50 } 51 52 // oplogSize returns the default size in MB for the mongo oplog 53 // based on the directory of the mongo database. 54 // 55 // The size of the oplog is calculated according to the 56 // formula used by Mongo: 57 // http://docs.mongodb.org/manual/core/replica-set-oplog/ 58 func oplogSize(dir string) (int, error) { 59 if hostWordSize == 32 { 60 // "For 32-bit systems, MongoDB allocates about 48 megabytes 61 // of space to the oplog." 62 return 48, nil 63 } 64 65 // "For 64-bit OS X systems, MongoDB allocates 183 megabytes of 66 // space to the oplog." 67 if runtimeGOOS == "darwin" { 68 return 183, nil 69 } 70 71 // FIXME calculate disk size on Windows like on Linux below. 72 if runtimeGOOS == "windows" { 73 return minOplogSizeMB, nil 74 } 75 76 avail, err := availSpace(dir) 77 if err != nil { 78 return -1, err 79 } 80 size := int(avail * 0.05) 81 if size < minOplogSizeMB { 82 size = minOplogSizeMB 83 } else if size > maxOplogSizeMB { 84 size = maxOplogSizeMB 85 } 86 return size, nil 87 } 88 89 // fsAvailSpace returns the available space in MB on the 90 // filesystem containing the specified directory. 91 func fsAvailSpace(dir string) (avail float64, err error) { 92 var stderr bytes.Buffer 93 cmd := exec.Command("df", dir) 94 cmd.Stderr = &stderr 95 out, err := cmd.Output() 96 if err != nil { 97 err := fmt.Errorf("df failed: %v", err) 98 if stderr.Len() > 0 { 99 err = fmt.Errorf("%s (%q)", err, stderr.String()) 100 } 101 return -1, err 102 } 103 lines := strings.Split(strings.TrimSpace(string(out)), "\n") 104 if len(lines) < 2 { 105 logger.Errorf("unexpected output: %q", out) 106 return -1, fmt.Errorf("could not determine available space on %q", dir) 107 } 108 fields := strings.Fields(lines[1]) 109 if len(fields) < 4 { 110 logger.Errorf("unexpected output: %q", out) 111 return -1, fmt.Errorf("could not determine available space on %q", dir) 112 } 113 kilobytes, err := strconv.Atoi(fields[3]) 114 if err != nil { 115 return -1, err 116 } 117 return float64(kilobytes) / 1024, err 118 } 119 120 // preallocFiles preallocates n data files, zeroed to make 121 // up the specified sizes in bytes. The file sizes must be 122 // multiples of 4096 bytes. 123 // 124 // The filenames are constructed by appending the file index 125 // to the specified prefix. 126 func preallocFiles(prefix string, sizes ...int) error { 127 var err error 128 var createdFiles []string 129 for i, size := range sizes { 130 var created bool 131 filename := fmt.Sprintf("%s%d", prefix, i) 132 created, err = preallocFile(filename, size) 133 if created { 134 createdFiles = append(createdFiles, filename) 135 } 136 if err != nil { 137 break 138 } 139 } 140 if err != nil { 141 logger.Debugf("cleaning up after preallocation failure: %v", err) 142 for _, filename := range createdFiles { 143 if err := os.Remove(filename); err != nil { 144 logger.Errorf("failed to remove %q: %v", filename, err) 145 } 146 } 147 } 148 return err 149 } 150 151 // preallocFileSizes returns a slice of file sizes 152 // that make up the specified total size, exceeding 153 // the specified total as necessary to pad the 154 // remainder to a multiple of 4096 bytes. 155 func preallocFileSizes(totalSize int) []int { 156 // Divide the total size into 512MB chunks, and 157 // then round up the remaining chunk to a multiple 158 // of 4096 bytes. 159 const maxChunkSize = 512 * 1024 * 1024 160 var sizes []int 161 remainder := totalSize % maxChunkSize 162 if remainder > 0 { 163 aligned := remainder + preallocAlign - 1 164 aligned = aligned - (aligned % preallocAlign) 165 sizes = []int{aligned} 166 } 167 for i := 0; i < totalSize/maxChunkSize; i++ { 168 sizes = append(sizes, maxChunkSize) 169 } 170 return sizes 171 } 172 173 // doPreallocFile creates a file and writes zeroes up to the specified 174 // extent. If the file exists already, nothing is done and no error 175 // is returned. 176 func doPreallocFile(filename string, size int) (created bool, err error) { 177 if size%preallocAlign != 0 { 178 return false, fmt.Errorf("specified size %v for file %q is not a multiple of %d", size, filename, preallocAlign) 179 } 180 f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) 181 // TODO(jam) 2014-04-12 https://launchpad.net/bugs/1306902 182 // When we support upgrading Mongo into Replica mode, we should 183 // start rewriting the upstart config 184 if os.IsExist(err) { 185 // already exists, don't overwrite 186 return false, nil 187 } 188 if err != nil { 189 return false, fmt.Errorf("failed to open mongo prealloc file %q: %v", filename, err) 190 } 191 defer f.Close() 192 for written := 0; written < size; { 193 n := len(zeroes) 194 if n > (size - written) { 195 n = size - written 196 } 197 n, err := f.Write(zeroes[:n]) 198 if err != nil { 199 return true, fmt.Errorf("failed to write to mongo prealloc file %q: %v", filename, err) 200 } 201 written += n 202 } 203 return true, nil 204 }