From 6bc9380b2ce47f7ac3316204fe26e42b600b0103 Mon Sep 17 00:00:00 2001 From: Arindam Nayak Date: Tue, 17 Mar 2020 16:17:22 +0530 Subject: [PATCH] Removed java-hadoop test and py-vtdb , PYTHONPATH Signed-off-by: Arindam Nayak --- .dockerignore | 1 - .gitignore | 1 - build.env | 1 - dev.env | 24 - docker/bootstrap/Dockerfile.common | 1 - docker/test/run.sh | 3 +- java/hadoop/pom.xml | 125 ----- .../src/main/java/io/vitess/hadoop/README.md | 96 ---- .../java/io/vitess/hadoop/RowWritable.java | 92 ---- .../main/java/io/vitess/hadoop/SplitQuery.png | Bin 33951 -> 0 bytes .../java/io/vitess/hadoop/VitessConf.java | 129 ----- .../io/vitess/hadoop/VitessInputFormat.java | 126 ----- .../io/vitess/hadoop/VitessInputSplit.java | 69 --- .../io/vitess/hadoop/VitessRecordReader.java | 149 ------ .../java/io/vitess/hadoop/MapReduceIT.java | 250 ---------- java/pom.xml | 1 - py/vtdb/__init__.py | 36 -- py/vtdb/prefer_vtroot_imports.py | 53 -- py/vtproto/__init__.py | 0 py/vtproto/vttest_pb2.py | 206 -------- py/vtproto/vttest_pb2_grpc.py | 3 - py/vttest/__init__.py | 18 - py/vttest/environment.py | 116 ----- py/vttest/init_data_options.py | 34 -- py/vttest/local_database.py | 469 ------------------ py/vttest/mysql_db.py | 53 -- py/vttest/mysql_db_mysqlctl.py | 92 ---- py/vttest/mysql_flavor.py | 114 ----- py/vttest/run_local_database.py | 212 -------- py/vttest/sharding_utils.py | 80 --- py/vttest/vt_processes.py | 231 --------- 31 files changed, 1 insertion(+), 2784 deletions(-) delete mode 100644 java/hadoop/pom.xml delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/README.md delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/RowWritable.java delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/SplitQuery.png delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/VitessConf.java delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/VitessInputFormat.java delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/VitessInputSplit.java delete mode 100644 java/hadoop/src/main/java/io/vitess/hadoop/VitessRecordReader.java delete mode 100644 java/hadoop/src/test/java/io/vitess/hadoop/MapReduceIT.java delete mode 100644 py/vtdb/__init__.py delete mode 100644 py/vtdb/prefer_vtroot_imports.py delete mode 100644 py/vtproto/__init__.py delete mode 100644 py/vtproto/vttest_pb2.py delete mode 100644 py/vtproto/vttest_pb2_grpc.py delete mode 100644 py/vttest/__init__.py delete mode 100644 py/vttest/environment.py delete mode 100644 py/vttest/init_data_options.py delete mode 100644 py/vttest/local_database.py delete mode 100644 py/vttest/mysql_db.py delete mode 100644 py/vttest/mysql_db_mysqlctl.py delete mode 100644 py/vttest/mysql_flavor.py delete mode 100755 py/vttest/run_local_database.py delete mode 100644 py/vttest/sharding_utils.py delete mode 100644 py/vttest/vt_processes.py diff --git a/.dockerignore b/.dockerignore index 58354ff4211..98cca97e5d5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ java/*/bin php/vendor releases /dist/ -/py-vtdb/ /vthook/ /bin/ /vtdataroot/ diff --git a/.gitignore b/.gitignore index d457f289e2b..041443eeedd 100644 --- a/.gitignore +++ b/.gitignore @@ -78,7 +78,6 @@ releases .vagrant /dist/ -/py-vtdb /vthook/ /bin/ /vtdataroot/ diff --git a/build.env b/build.env index 467749a10f1..fa83ff30ea1 100755 --- a/build.env +++ b/build.env @@ -32,7 +32,6 @@ mkdir -p "$VTDATAROOT" # Set up required soft links. # TODO(mberlin): Which of these can be deleted? -ln -snf "$PWD/py" py-vtdb ln -snf "$PWD/go/vt/zkctl/zksrv.sh" bin/zksrv.sh ln -snf "$PWD/test/vthook-test.sh" vthook/test.sh ln -snf "$PWD/test/vthook-test_backup_error" vthook/test_backup_error diff --git a/dev.env b/dev.env index f256abbd93a..f1e6e71b896 100644 --- a/dev.env +++ b/dev.env @@ -24,30 +24,6 @@ source ./build.env export VTPORTSTART=15000 -# Add all site-packages or dist-packages directories below $VTROOT/dist to $PYTHONPATH. -BACKUP_IFS="$IFS" -IFS=" -" -# Note that the escaped ( ) around the -or expression are important. -# Otherwise, the -print would match *all* files. -for pypath in $(find "$VTROOT/dist" \( -name site-packages -or -name dist-packages \) -print); do - PYTHONPATH=$(prepend_path "$PYTHONPATH" "$pypath") -done -IFS="$BACKUP_IFS" - -PYTHONPATH=$(prepend_path "$PYTHONPATH" "$VTROOT/py-vtdb") -PYTHONPATH=$(prepend_path "$PYTHONPATH" "$VTROOT/dist/selenium") -PYTHONPATH=$(prepend_path "$PYTHONPATH" "$VTROOT/test") -PYTHONPATH=$(prepend_path "$PYTHONPATH" "$VTROOT/test/cluster/sandbox") -export PYTHONPATH - -# Ensure bootstrap.sh uses python2 on systems which default to python3. -command -v python2 >/dev/null && PYTHON=python2 || PYTHON=python -export PYTHON -command -v pip2 >/dev/null && PIP=pip2 || PIP=pip -export PIP -command -v virtualenv2 >/dev/null && VIRTUALENV=virtualenv2 || VIRTUALENV=virtualenv -export VIRTUALENV # Add chromedriver to path for Selenium tests. PATH=$(prepend_path "$PATH" "$VTROOT/dist/chromedriver") diff --git a/docker/bootstrap/Dockerfile.common b/docker/bootstrap/Dockerfile.common index 6a9255b38e7..4f7f22eb3a1 100644 --- a/docker/bootstrap/Dockerfile.common +++ b/docker/bootstrap/Dockerfile.common @@ -40,7 +40,6 @@ RUN mkdir -p /vt/src/vitess.io/vitess/dist && \ ENV VTROOT /vt/src/vitess.io/vitess ENV VTDATAROOT /vt/vtdataroot ENV VTPORTSTART 15000 -ENV PYTHONPATH $VTROOT/dist/grpc/usr/local/lib/python2.7/site-packages:$VTROOT/dist/py-mock-1.0.1/lib/python2.7/site-packages:$VTROOT/py-vtdb:$VTROOT/dist/selenium/lib/python2.7/site-packages ENV PATH $VTROOT/bin:$VTROOT/dist/maven/bin:$VTROOT/dist/chromedriver:$PATH ENV USER vitess diff --git a/docker/test/run.sh b/docker/test/run.sh index df0c6a20f82..539a7fa57e0 100755 --- a/docker/test/run.sh +++ b/docker/test/run.sh @@ -165,14 +165,13 @@ bashcmd="" if [[ -z "$existing_cache_image" ]]; then # Construct "cp" command to copy the source code. - bashcmd=$(append_cmd "$bashcmd" "cp -R /tmp/src/!(vtdataroot|dist|bin|lib|vthook|py-vtdb) . && cp -R /tmp/src/.git .") + bashcmd=$(append_cmd "$bashcmd" "cp -R /tmp/src/!(vtdataroot|dist|bin|lib|vthook) . && cp -R /tmp/src/.git .") fi # Reset the environment if this was an old bootstrap. We can detect this from VTTOP presence. bashcmd=$(append_cmd "$bashcmd" "export VTROOT=/vt/src/vitess.io/vitess") bashcmd=$(append_cmd "$bashcmd" "export VTDATAROOT=/vt/vtdataroot") -bashcmd=$(append_cmd "$bashcmd" "export PYTHONPATH=/vt/src/vitess.io/vitess/dist/grpc/usr/local/lib/python2.7/site-packages:/vt/src/vitess.io/vitess/dist/py-mock-1.0.1/lib/python2.7/site-packages:/vt/src/vitess.io/vitess/py-vtdb:/vt/src/vitess.io/vitess/dist/selenium/lib/python2.7/site-packages") bashcmd=$(append_cmd "$bashcmd" "mkdir -p dist; mkdir -p bin; mkdir -p lib; mkdir -p vthook") bashcmd=$(append_cmd "$bashcmd" "rm -rf /vt/dist; ln -s /vt/src/vitess.io/vitess/dist /vt/dist") diff --git a/java/hadoop/pom.xml b/java/hadoop/pom.xml deleted file mode 100644 index 37e815dde24..00000000000 --- a/java/hadoop/pom.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - 4.0.0 - - io.vitess - vitess-parent - 5.0-SNAPSHOT - - vitess-hadoop - - - - com.google.guava - guava - - - com.google.protobuf - protobuf-java - - - - io.vitess - vitess-client - - - io.vitess - vitess-client - test-jar - test - - - io.vitess - vitess-grpc-client - - - - joda-time - joda-time - - - - org.apache.hadoop - hadoop-common - - - org.apache.hadoop - hadoop-mapreduce-client-core - - - org.apache.hadoop - hadoop-mapreduce-client-jobclient - - test-jar - test - - - - com.google.code.gson - gson - - - - - junit - junit - test - - - - - org.apache.hadoop - hadoop-client - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.1 - - false - true - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.22.1 - - - - integration-test - verify - - - - - false - true - - io.vitess.client.TestEnv - grpc_port - io.vitess.client.grpc.GrpcClientFactory - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - io.vitess:vitess-grpc-client - - org.apache.hadoop:hadoop-client - - - - - - diff --git a/java/hadoop/src/main/java/io/vitess/hadoop/README.md b/java/hadoop/src/main/java/io/vitess/hadoop/README.md deleted file mode 100644 index 9e4a083bd4c..00000000000 --- a/java/hadoop/src/main/java/io/vitess/hadoop/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Hadoop Integration - -This package contains the necessary implementations for providing Hadoop support on Vitess. This allows mapreducing over tables stored in Vitess from Hadoop. - -Let's look at an example. Consider a table with the following schema that is stored in Vitess across several shards. - -``` -create table sample_table ( -id bigint auto_increment, -name varchar(64), -keyspace_id bigint(20) unsigned NOT NULL, -primary key (id)) Engine=InnoDB; -``` - -Let's say we want to write a MapReduce job that imports this table from Vitess to HDFS where each row is turned into a CSV record in HDFS. - -We can use [VitessInputFormat](https://github.com/vitessio/vitess/blob/master/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputFormat.java), an implementation of Hadoop's [InputFormat](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapred/InputFormat.html), for that. With VitessInputFormat, rows from the source table are streamed to the mapper task. Each input record has a [NullWritable](https://hadoop.apache.org/docs/current/api/org/apache/hadoop/io/NullWritable.html) key (no key, really), and [RowWritable](https://github.com/vitessio/vitess/blob/master/java/hadoop/src/main/java/io/vitess/hadoop/RowWritable.java) as value, which is a writable implementation for the entire row's contents. - -Here is an example implementation of our mapper, which transforms each row into a CSV Text. - -```java -public class TableCsvMapper extends - Mapper { - @Override - public void map(NullWritable key, RowWritable value, Context context) - throws IOException, InterruptedException { - Row row = value.getRow(); - StringBuilder asCsv = new StringBuilder(); - asCsv.append(row.getInt("id")); - asCsv.append(","); - asCsv.append(row.getString("name")); - asCsv.append(","); - asCsv.append(row.getULong("keyspace_id")); - context.write(NullWritable.get(), new Text(asCsv.toString())); - } -} -``` - -The controller code for this MR job is shown below. Note that we are not specifying any sharding/replication related information here. VtGate figures out the right number of shards and replicas to fetch the rows from. The MR author only needs to worry about which rows to fetch (query), how to process them (mapper) and the extent of parallelism (splitCount). - -```java -public static void main(String[] args) { - Configuration conf = new Configuration(); - Job job = Job.getInstance(conf, "map vitess table"); - job.setJarByClass(VitessInputFormat.class); - job.setMapperClass(TableCsvMapper.class); - String vtgateAddresses = "localhost:15011,localhost:15012,localhost:15013"; - String keyspace = "SAMPLE_KEYSPACE"; - String query = "select id, name from sample_table"; - int splitCount = 100; - VitessInputFormat.setInput(job, vtgateAddresses, keyspace, query, splitCount); - job.setMapOutputKeyClass(NullWritable.class); - job.setMapOutputValueClass(Text.class); - ...// set reducer and outputpath and launch the job -} -``` - -Refer [this integration test](https://github.com/vitessio/vitess/blob/master/java/hadoop/src/test/java/io/vitess/hadoop/MapReduceIT.java) for a working example a MapReduce job on Vitess. - -## How it Works - -VitessInputFormat relies on VtGate's [SplitQuery](https://github.com/vitessio/vitess/blob/21515f5c1a85c0054ddf7d2ff068702670ab93b5/proto/vtgateservice.proto#L98) RPC to obtain the input splits. This RPC method accepts a SplitQueryRequest which consists of an input query and the desired number of splits (splitCount). SplitQuery returns SplitQueryResult, which has a list of SplitQueryParts. SplitQueryPart consists of a KeyRangeQuery and a size estimate of how many rows this sub-query might return. SplitQueryParts return rows that are mutually exclusive and collectively exhaustive - all rows belonging to the original input query will be returned by one and exactly one SplitQueryPart. - -VitessInputFormat turns each SplitQueryPart into a mapper task. The number of splits generated may not be exactly equal to the desired split count specified in the input. Specifically, if the desired split count is not a multiple of the number of shards, then VtGate will round it up to the next bigger multiple of number of shards. - -In addition to splitting the query, the SplitQuery service also acts as a gatekeeper that rejects queries unsuitable for MapReduce. Queries with potentially expensive operations such as Joins, Group By, inner queries, Distinct, Order By, etc are not allowed. Specifically, only input queries of the following syntax are permitted. - -``` -select [list of columns] from table where [list of simple column filters]; -``` - -There are additional constraints on the table schema to ensure that the sub queries do not result in full table scans. The table must have a primary key (simple or composite) and the leading primary key must be of one of the following types. -``` -VT_TINY, VT_SHORT, VT_LONG, VT_LONGLONG, VT_INT24, VT_FLOAT, VT_DOUBLE -``` - -#### Split Generation - -Here's how SplitQuery works. VtGate forwards the input query to randomly chosen ‘rdonly’ vttablets in each shard with a split count, M = original split count / N, where N is the number of shards. Each vttablet parses the query and rejects it if it does not meet the constraints. If it is a valid query, the tablet fetches the min and max value for the leading primary key column from MySQL. Split the [min, max] range of into M intervals. Construct subqueries by appending where clauses corresponding to PK range intervals to the original query and return it to VtGate. VtGate aggregates the splits received from tablets and constructs KeyRangeQueries by appending KeyRange corresponding to that shard. The following diagram depicts this flow for a sample request of split size 6 on a cluster with two shards. - -![Image](SplitQuery.png) - -## Other Considerations - -1. Specifying splitCount - Each split is a streaming query executed by a single mapper task. splitCount determines the number of mapper tasks that will be created and thus the extent of parallelism. Having too few, but long-running, splits would limit the throughput of the MR job as a whole. Long-running splits also makes retries of individual tasks failures more expensive as compared to leaner splits. On the other side, having too many splits can lead to extra overhead in task setup and connection overhead with VtGate. So, identifying the ideal split count is a balance between the two. - -2. Joining multiple tables - Currently Vitess does not currently mapping over joined tables. However, this can be easily achieved by writing a multi-mapper MapReduce job and performing a reduce-side join in the MR job. - -3. Views - Database Views are not great for full-table scans. If you need to map over a View, consider mapping over the underlying tables instead. - - - - - - - diff --git a/java/hadoop/src/main/java/io/vitess/hadoop/RowWritable.java b/java/hadoop/src/main/java/io/vitess/hadoop/RowWritable.java deleted file mode 100644 index ca91d512ed9..00000000000 --- a/java/hadoop/src/main/java/io/vitess/hadoop/RowWritable.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2019 The Vitess Authors. - - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - - * http://www.apache.org/licenses/LICENSE-2.0 - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.vitess.hadoop; - -import com.google.common.collect.Lists; -import com.google.common.io.BaseEncoding; -import com.google.gson.Gson; - -import io.vitess.client.cursor.Row; -import io.vitess.proto.Query; -import io.vitess.proto.Query.Field; - -import org.apache.hadoop.io.Writable; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class RowWritable implements Writable { - - private Row row; - - public RowWritable() { - } - - public RowWritable(Row row) { - this.row = row; - } - - public Row get() { - return row; - } - - @Override - public void write(DataOutput out) throws IOException { - out.writeInt(row.getFields().size()); - for (Field field : row.getFields()) { - out.writeUTF(BaseEncoding.base64().encode(field.toByteArray())); - } - out.writeUTF(BaseEncoding.base64().encode(row.getRowProto().toByteArray())); - } - - @Override - public void readFields(DataInput in) throws IOException { - int numFields = in.readInt(); - List fields = Lists.newArrayListWithCapacity(numFields); - for (int i = 0; i < numFields; i++) { - fields.add(Field.parseFrom(BaseEncoding.base64().decode(in.readUTF()))); - } - Query.Row rowProto = Query.Row.parseFrom(BaseEncoding.base64().decode(in.readUTF())); - row = new Row(fields, rowProto); - } - - @Override - public String toString() { - List fields = row.getFields(); - - Map map = new HashMap<>(); - for (int i = 0; i < fields.size(); i++) { - String key = fields.get(i).getName(); - // Prefer the first column if there are duplicate names. - // This matches JDBC ResultSet behavior. - if (!map.containsKey(key)) { - try { - map.put(key, row.getRawValue(i + 1).toStringUtf8()); - } catch (SQLException exc) { - map.put(key, exc.toString()); - } - } - } - - return new Gson().toJson(map); - } -} diff --git a/java/hadoop/src/main/java/io/vitess/hadoop/SplitQuery.png b/java/hadoop/src/main/java/io/vitess/hadoop/SplitQuery.png deleted file mode 100644 index 1040e0b2ae637a3e17c466733bd807423d4e11e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33951 zcmcG#cU)6l^Dk-xDI!e-sR9B5(xi9k9h4%7RFNjVhENql?;Qn#B2~Hw1QI|(uaP1_ zs-a6s=mA2xi9XMJ-gD0He(qoQpX_Aqz1Ga0nf0BrCNYooH7Lj!$u3>GM4_drYIy0= zW%Q*>SL8{E@gt`uIog*ly}qQSs$?86wb??c3)^^x1?KxWOvk9%3%K{h`dM*h8oSnf zGlw^8ki_ImMCTfau> zEwQAc^$s1aZUEq?<;=3*i0D9;kL7C9$_fw+bjS|7F>l~~-C^b3DncF`5`x)izL3^1 zk2sz-4w(NAxD|S&5tbSg6Z5;h3?n~p_}a2bk5;S}G_DCzJlDzRV?OJkqot)CdrRAy z1J@&o$iY8~zkc)-6%ly?A^qpdI|F(Qd6%7ie+EQFCW(JOBIn=he@%+Wfe&?ds-n0- z|6c!{^cccBqpPcX%U${YKiB_IipU|3`b+bFr2St^kh0%$=jP@nll*)A&lmi_e+Uu? z{~sK%q~}wX+>Np~}BlkBO<*JqtAwyE`{^+yIrUOSmhtnL;`bbhRf% z9As7>^%pyR`y=Q^*OM9RGdmQ8hi*?Snk@|FK`vBe7~wG-GQBbXCZ!t}cmq^?r?dDc z=sAW*7qat=pX@eX;&`1wyPenGvNel}we19jF`gW2gssy8@-N^eq4K-TMIa_{An)5m zD*nV;+vOUNUBPgMDkRN91@luvpHW~Y>>Q8ulwd+2wSA8-ig>Il$<*O1V8s!)sU^<9H`AyWwZ zH@1`3J~(~LZ#}i+x97nZQ;-!v2&QvZB)ae}*+fvt~=;RaZ@^~4c8ITiVYsfTTO6mKjUPd_hJh$U=li-9m$3_?VJ0= z6a}gavGEMzka{6)9IKv$vuw_q;qCse>7K?<@Sk`D$woj&!B@( z$)Tl9AR$U> zSJ^iM5>h9$0m`4)H=q4$!rCNZ%8%;jX+lbEkJD5k2|Ka6e8E)OTWmiR4EM*( z4XEHmJL7hq`BdT{fX;!J21RZh4O%$Gc&Eeh2Mzk(M{iJP3Y%?ynPe3rc4O*$TNvgX z=X4LWOrBVuAin?h6Vp{{GPiG#WM`LK?gV^76F;yDKYiU;F~=JJ72}q{IkPt1;8m}|NLnt1EH`0GKah%44+682iwk9>2dY&iVTGM-Y-aX^0)v5?j7kafx z48uXiKkUnJM^j&We!cwo#T~AhwUP#P3wpOV+b3ru+f&;i`+h2(JBrGSwh1J3?(wdX ztE)j5GBU9D@C4i*6ZK(8^nI(1aIyE_k(*ITBS{xjsEOJuwr|ILuQjpcK6OJ)Tn>KU z_FmXYn9rECp7j#mDwI)GO5xb@kY4{zPhM2F4L6Cn`p1n^7u}XoD*oc@N|VN4w|?=E zd`JT>NfFN@%dBMHKO>@=SJ7m{zTR7W|Fq&W`wZVGgx-Q4%*9A8bHYrDSb7r2Ocqk4!epsVO;1{d`fpTk%8A6$)vw^~Va^Fvf_Qgd1|@c?BAHSbq$0 z$^B*D{y}3ouI*A>pv@HhD-AW3qs=Q1gpa7ypr7f_qac$+3fnAUA8*4NR!5H~B;sA^ zpO8Mk)x|A8k$;nzx`DjrQFL$;ZS_to>($F|J%7L8IsEB6EXsH%pz@f$;kC~Al?POA z&`abXS=Y>b1-CJTZmL>0B>QPT1O&5xMp{gì+dM7 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%0DAlP zeSehk_q3k(=xX2aZ0DM{d12Ns(ErFIdiK%JoK$a-$B~{q^2M_=(6+-ybZik^o;$3k zr8(Y?+ww@6@6%{>4erHsgsHZG%_rq3yO9iERH{(mCnMQvj&L>Hw`}<}hLsd1!(B*VlnP!@lFimpl z_4myoAKFevV^>LyV-vMo%L<}Y88s$|_-8X1El74oTk0GwcVzZ`XhPo6*dkj>%Xw$~ z7ZY-4>Z<%FG2waL6U*-;ENYMksbza4XY1HA;qrkb93(NhJr(Q#4_7gD(m@>#rBp`tvq?9KUhZ5@|7@lW0=X@UpN= z65kkUsi@pW|3-;+Ga<( zPV3`TbsBxk-z@!_bg)l8yqiH)0YP21$2$^*M2&c$-hD?-+`8YiP@f82KH;0`F(knj zdQN>m{tUc19XJ7R=D}2JDa~Z;x%K0U;M=0yLWeYgygDzO{O$NG^})YYheKAvXJaZn zOFE@CdyEVHsh3s-%}iSaibs6!B3;uqbQNnJ`1JzGs(|$Q>=||JZEhCE4v+d_6J69s zY-Dp-ZrZ7|(6Xy>#g}t^P_~m+^qcwtcad$vb4NC+*MwHXiHavIcF>@W-N2vYT1;A_ zmYthB^jxm}AC|^yVvMviwsz zqNdw5uGTbDx153Q8@eeZyQbjxO~q!eBJ|DMD?^oxDR~)eAzd&l$p{2vyfo(rPVZ_r z^>4$LRQ(MC-_nDTkxAqKPi1czR>k*5fvQM%cS|GP-60{RbhmUjNOyNPigd}LrMr=m z@X&bqY|-iC|+uF=937y<&%L*82gnCHe?sSW1vpn6P26 zbLKYEvXGmp5hf`ERkSlS3n8QelEj#W0&^WXSNEi$y|EpG&+Uj~_-;&VcF9(TJx^i! z@kaV&wv+I^j_YXqpu6G$OT+JV?4|?!977K9xHR zd)Epwvbh|?H6Kh{9xNzxoHN#6$-;Zj3qSV_IvM_*F*Dq zg!@{4Ih93QL}FSF>1u;_?1mN0>+y9JFTA6TdAkwM@pSwX_$DarOKXxx$2c3V*(}FY zxvfALGOY5>ev=1_XP1U~wLy#Q<#r>>Z$a{1X^MP#M!;#tHv5jAG-qEvJND|FirR;3 zQp>UiSGGHK=IYNiF%=I)^6~C;HG?IO8>W6@iAr6DuDuEt?;ra2n$Vfpcb8Mk(Au{o zZCAy`Q`A5n&a1&daU8#YgXC8N7z!wl7K=Gz?i-#*aNcZO zFMpZ3k7{Cgpv$AxK$sT6*26DW7*AZ?5BMBAvzXCr)AH4!!{m=zrUYnTx|Vx8MtWBe z>dRup;!R!gs;+(ORCuYJhJmfv4I` zk`r7$e`5&t%*8jjJvgMO4C017OYp4+>A}*C_~WNGm^TXZ_ZId-Y)uw*53fnON;d@M zb|{_#n&qWSKAgJdLDKVMKy~L)QCX^!zBW5bq*MWH{)tX68}bGogcgS}*w&Lu8t@rq zI2ektI*7D3a)l$-nlwARNM3V?CfjC@yl+LxY2Lc@)da}JYB=v3oF8Z#gAjLgt#+P1 z3DT2e7hEE`+GN?bOmwQsYc+=;2fSA-_$Cc`(hbYhJmUYZ1b&Fm<@r3d2L zEhu!q)RK0Of6>+j9(ES&u;Wr<_%0KFF*G}?QJxvj)1gNZS`#n+(cA>}&bB2f)6kpf zJ)M~$*11)XPZrDif6bm5wtY)%O?B465y37Tz(CFveOp%9f4KPy4#0Q{SNpRE#bb? zo$!@mpd|Oa$bZ2t|H#*CvzWby_!|%PW)r3eV!Kcu0twArjAMiCs9k>lMM1TNX|0-PhxTJscf`6mv2KGHx~^kdCUZWcFa%N9Emtg?S1n zw_%IR9K2mH13pA!*nNbP>2j5={Y|3V8WJqcyRZuKe6@mcZlJwuq*-t}kB@)->|yNG z$)8em&dEG{zyGsu{TKpeuRT$+uviVBMwopgbyLQ1t+;QdpcEoLmc;;#ahpg{4y;|>jNkJsg@+vns_Q3q4{ zm!I$)8TPg6{52rPq>tM}+Y_-#@7qm=B@F9PV18`VT-2K@7XzR8zmVWV3HSAqhJGc- z8jUEx@Rl>&pnmZ#_z*|!PC+dqX;+H%KfjE(rCJ{+V*-dT#lAU`77nCJ*p8jBsHU@v>xB3mmT2dQlwGrA<6x+Y*l*Seis z+a4a0Scb6ygh?Nvk_l=z4`K;|7gI&21fW^s?2m#7%MX+Yb^9-oi&{lZj4IQdG} z@#_x0qsMb+`qOo&653-dKxG9kT%r2Zo2Aa;X6}hPh5;wb-jVGLRggkIwOs<&+^E!; zVn>=8JLae0N*e~YgN`=O-hxhB?aoXHAlbN6YW==o=+1SKN8;NZh3qsL8!VXYR+u~H z6fV5I#osdhLCi=2HSnFx-=%W8!i=77M5AbDuMQK%aX|tv4f;|I&lE>9PN1$kr%1q# z4iCF|$6#yN=@XEj0P0?ib8CApUf?BVzM=K@EDY;hv)d?B(uATK!5MiyHw_aS-13aq z?~6D~D(H7GOc^$IH9ZVQfS*50=Oq~lS|f&cv+sa zAo}N}9<<$MB-sAz|OlcSP7*SBo#2 zZ5JrBmOjq_-*pN^+@_Qk3oJyz1?Ym^qF`<^VoW1Zhy$T`fu)o^%&j5wloLamxH)2^t z*=ZB06QvlwqN}!HFhgqA$OAn*JP#4KFjcw_LjrYzM0NsMupQUgbsn}sc}$gQeDkkZ zTj&LkiplS~sNdAiwuotpC63`i%HYge%O}{0_WusV3JfB{LiUlGM~|P8z#E=}n)~f# zj@C3Mqm}y>2Du7f3pzAA4`#E5v{D?|QM^!7Q&D_aX~d|)OXg-i3%<9wPViGP?MeA? z(J3RV&tG^7ItXZcimB)*y&JX$Dtf`Pa1lr5!D^xiP&tiXnXB;!S<1M(_b8kZEVi=x z$(MN@7Q!9aIPyI*e?QbT^RvuW(P3wz&LicgTtrRZ?7q03C~CN?=}oPrZW1NnLOdH} zR}q8W#sAd`6Fuac`W3P;TTYbg&9=X2?vR-!3M*p?(h?7Uh^{}0LG-aF#qKzddv-(* z`=e;*IgeEKIW0V_?=-Sov(}rRt*3(WLBww&rPFt%E14n3L+ptpaG~JngX>u2b~t_j-@}6P)mq@$Q8;(Z5xhb zaQ4hzYWD7!-x?_E6gu*%Y8k&2b31pN{OyQ7((pumEq)5T!_^o&%!(igf52`!3}GVK zp6es!0TVshtA}+Gytm**BDvR1S%O0j?~oT+5hO%2Yj`kx=&SK_>xUysK~NN{C|Mr? zXrtCWA*mOLLgV#*|6> z;Yic%2V#GHd?12u)%jzb(3Y$H^?ttW=^>&fjlOv3k!wXP>uMQ-g&~KM>0|J|6kNbw z^saH!iDCDX*rkwAO&SC=xV*}0ZbSHl2i(pk_~Y+3Oj!dRwC%`O4ALDYwX z)u;7;EH7jtJSX|CFNO|~A=!_dv}FW41{|^{i1A6&Ty}sjg@ks)ebt{UN&^lxn;czf zlCM**d_FzXJk?DQ0I~*jfGTHgPW7VlT+3;v5IVL!ULo=v`}m|i>|hRv-V-Fv-=Pl8 z-x+7|mQC3N{e(K$kysO8ODSN{V=DdJ%uUksTV>$1#I{XckwGeMaEAo3o3fUEcAza= zHM!|C9!MJuveJDQCgOph6vdZeHp!K%K;HW!wu;j-rHKsSfRl(T*FME$g>a9ZP@`KY zi`W~3GP8LBHXVvo!b_wz9;X+9T~jN!p91dJnY(0dxk7Gz;3#}?Wem8{Qz6ABYyzQu z<}Uy}FFInhVR?*uSW>*Kjo7WXPr^Vp9!mpXW2>R>moksfJ*DIP%X_1nCi*VU_L)wAa_fs{c^i~-cfQBovr^|HYHaKc4u9#pP*O0LJSIj>#WVU)4q-lm z213|T>syeD42|(tlIlk0jzhpQZPhW>-nx$x4s7 z@JdOMK+4HVZ~_OlyH9`DbZOfZ9VG#j>u+ZjKiQx8hs)WkK1F=w`Ed0RbhE^2?!nme zk7fO11Lo9#gri{$ku&!SyzcS4PV6~r<@w;D{O68c=iF+!!S!cyq|?j27lByN#Ya|s z57r*$4c4Bpg+wv*_a!)UM7%?8E|P`(h>vL>G42pwo7!}`?7{{KjUNd!jHQnH*_g=@ zie`{_+0X+`WDslKHz;IGQd~f2#m`+b$`R-Q3jNrYf`UUYR}nvZ_tjk0UC*r_Nf83( ztxIpt(K|tM#(pPn^Bn36dd42Wjd7nfC2WDfzFqycA{aJ%%Og1ku)XyLIud2MIdyc( z&o+9Utckxk12lV`K;%kl9n%xXKo<0m+w-m6FK{n)MBUf~DjDG6j0rq>NT0j{dbYgm z!}9ba8Tv=ucN6)9{rtTpqg)dgN=VT>zLYQJA7{^aB>P@Tl89xP0KsG#EU;-C&t! zH%oi~945EdT_(FRgwZMg^S`o1Dczsg7;!la-Q+e}^l=gC*8al480A?$jXXQdlljjX zvjpr{VtEVD>ZLaQy6M#W8g|+6V=g`&35ZZm8QX3*QRrVa0@2$?0ps8AKCKOTITD+w1R~c7T%Y%i+5ERVHr-%`d991%U*^tQ z3Y=-1Yp~1ffd;%d#PF8TEDnM8e%b+PozO82 z-0DB5z404SCgMyqO`V6}%+TQDxY!ZSl<}wl4iI%VS2)Q02)EytNN+@6TEw?V=llPv z&~}p3uW`74UUta&CqD?Nh)C?4c7{3#2dXKGbd3AY(-ov1Hx{PSc>C{;AlxbG&k*w z=OlQBsq^O@rrw0>i(Z4uh5_T1;@PT7XWEmRlB2|w!N2t!U<`dh^rzT=nxXH`c0_ZS zA6Yfk#Q+xj%)xIh zP)p(pw4+~w_nPbR@-`rv=}UXYMU6DyH$nGSeDs|1Ju@)NfOC>C^3- zaMIoPUrvJ)pBEUU!b#Efc`*7&YHS{lh|vG9_M#EZV-+VbM}8~W`1w>&fGDTf1husQ75Y#hG}fI@T)h8}oL{Pq3yzr8=#LcpLc~WzLqr+% zjn~zeA|I|QAR0+sFHzKjCH6%)X~EBgI|%`bX-p^?*e$9#UW)o+$AW`? zD6t;Mbfwy!%q>-f0}p_r3W7$379UaLk;%?$ z0p(}LUiQ(lx8M?^Z_YFJ+Zf2b9|`3OldEu0FvH*&nwNmhiLK23C9TAW#QOhF`u*Sk?=1fx zJLJqCnC{r6q)+s9?>lkfWOC4u(+)hTD4&U46lZiW{OcP)u8@?$=?g*(;*2iaRB9jQ zs2DZ*fL5_-vdi>1_>korarehNK3tarmJyL?30e!AbY`~oFIl^*dZAOvg2mgKWtBQ; zEA_i`btc-{*sKAm8R#tazQ->UqTyeUr06;LI6S+Z`)Pj;n?Cb&CyF+mcTfHP3?Ay# z5f0K%Y+V|bde7l5gh+O2c`m*O`mvM%>(3}E0~)K!)!LC#?xG&Z(iW7a%fq97)9{0G zFZj6D7ev^ZC}-O>#tqC&?`^J3icD-wv`h@@iQeLT_UwE3e%roAyAg>9_mPMK4vpqH z(mL2du;DxWhBkO;|E%ORXa7k3kKyo!@a?JBW`E1y_`dKpTlj7k=f+ zt^_M=wKJybAi?i`3@3$_R+|RnpJw!;eDS7a$p>>^H+}*&AjO{cN&3WJ{tdd>o$l;L zLK|cMY}x|0pKSo88EUPoI0QaH>8!jHCn^zxJC1ThlTd&wfav}#-lB;{FhfL z@NljkDtNVOM=``gm;10*CW)&X<}ZJZ4|!n+wrYs3#;)~J|7XX;L;07|JUgoZTd{{9H<1n3{NV*0S9 z8bmlJWST^dF5e>#e~yAPZxgNLzD0p=V88ahnwL@+8+a0_+T8_|Zw^VT4_D#({>V&g8xf0j6-0E;aC?8r55ZEo7(H_{O59MDMO( zk_PAb0I6aKFPAOsTD_E=+*H<^1<6{fewIH9UC!j~c{ln#x7HLF8N(VQ!@-}Q4}+z7 zC2@yht9tb61*u4_z9*jgQ{Tfb?CU}!IHus#m$>30m}wMR)_x}dkePtb7wK?Cl8ZwR zNxEP2m-UgUgv7ZRT9NAHoX_HOTha+Z!I^-`Oo^vMh&85k^jSH%2L|=<14o#taF{ws zHzEK$^xXmNQK}TvEHsPKtV z>i1o7^NAo=ijub;?g5e^-`|^xv6bD(nvnwshW|uRR%S!VPKh4xJXz=_j*_@UewisM zIIPH8HjT$|{T2_n1dwn~)U3W`GHa~rj((}9yE)2U*|rx@ixAGNr&=|r-oMQ0b?Z&isHVhQXY$XfN)#H-l%g#IcGchO|{qFS!mTIVh0 zfY`-(&lA+Oj_*s&-M6fI9kfNuOs>veGX;ewVn5)voP_y;}v0KH)U|KASi_-1LtECHpI%@vX< zF(B(A5$)qrMgm}3SUno<9A4Z;g>vee(vkxS^ETjaoAbfhKwsIC0;Ce4a{mWyqXP1F z7+rDWt_*h%GoAvtwTmbL56sO8a(v65D6o*ropFJ{sji6`pi3b zDQhc1IsOQywW%7V@=6BA5o=^r?L6nu!9P6n7CopJ?u~IVqS*7V1Qcgp0VXRbUd{#A zhy~^n_I54ylJb4#-^|}uk^K>8f3u`qBzbP|EH;2VG2)X`V#>uzK@u3(^=H9J>^K#6 zw$csDemY+rkoIF6de&92A{+JMdk1&6(bmaNI(wm;)b|axxZ5=q)|?Vi_P2V(C9+l^ zuC|b=DaCX5_Pv`CfEC57 zer>U#qiq8=-~*9GKpNLW>-wZMVn@eNSibv`M#gPsA65V}AUS4s@4E0}-7%^@{W$t^ z)3J{%%SG!0`0re0!%ppT3*jz=S2oMJcPJ1iL?>fY%MY4%cf9Nq0jFW8*kVqvZa=!Q zlBq&;usCmt2NPxq{I4?|q>N2IdNMANVOZPTS7o~<-mlR`7%byQhD#KXj6wbr_bT?qo>pr`g0iRYvjUwN2 zXUcJ}>y`-9&+m59lKu5E_dltNrw^&OKGMVOt-!jXdgGD&^iYbrjPW6_1*dHkY;02d zR0^`3KPqZD8Xf_*$_MI|EV*Vd)i_3d@YD$GLgQts>%Qe3v_`2^uWQ?|!R;6u`yVerZC*7kLYkj>42i)qCUW_y$91%#?SYnt<9ehcqIS^aL&7-&6ZXSo|^Gd2A9J#z0lP8(!TcOL#Q!g2zy zso+$G4@grc?E;Kq8IQbL&VI@wxuygg_|*N*B*YY(csS)8qB`aMo`=`R()P_~y2?1w z$I}6?+8gGM4c1LmfG>7V{q}AR(I`L%+nV})g#XBc{rds>8lU)$z&pp)h0VBzeug2p zIFxj|018g*S%B*_@G#&hiww&;Hz>?qVyP8SE=b*e5V1C0UVfIuKCMZL8VEsh!2$og zJ5x4l&0XpkdcAhlTxCKwN__`OV)(i4YB1K+a%{BVu{59Z+I9^s%*Lvh4jS^En3l#J zo=F5(Zl-oezR`9R7m^oM9JCf)`Bq)7E6sV|+z9Yl6f3K@F8{hrDSSlnC^y=$Bg-e1 zlzKJ)FdW`hz!C1Cq!&ZTc3@R+KQ7ubD5Au-<99jaH=T>{OKL=Pm>3&{-}V!@fZymk zjB^gob*)n|mnNiJMZ;;CLO;n(gxjM&Pw3mt_esBelxVC7WG}wHjT0YFTEaOKWB#q$ zn}hq;IXZTclHq2*twk1MK$k<8U!kV#SE#}dFv~f-{NU%FC7AAJeY;{Bany+}*>nr# z1^WWFVmh$sJNq(mC8s;Kp6@@g`zECmc#omZ*5uf!jza^;HHaeP)k}b}qghiG8M9=1 z(WU_j-UV$aaRZh$6t4E4pB=>|K&joT%xia8V6n5>t}piS3GgA>U6&HzDF*+Vb^i_= zIx-BO3V0u<-X>GizI^Vw4&`o@5s#tWR*c-we!CQ691oK2vAsXHYWRhz3J>dFRFTQ! z{Q7E4GvR6dNh?gI05eQ(z1&DS9_=I6wQ0liq-PHwHqu%_j7lVjS`ekIAb0T(YC*k7DvH>DKX%lFHQhM7J*x4F@o-6y2T z+@)D;r3Ev>U?#xEVXj-d=a1~;3=t)|EfzF)NU|&3uOHuQ+ZJwc^$pTpne~GQRgN*N zkglF|7a;k&2rOi*SkJi#32KuuLsPBK9U5A;x8+h#c-1iyM1~p2Q!>$6nlmrV4ZKYw zn0%mG2@T@41Y_e}PS?cF!uw%xx@eQ}?4l29=di)>4B{XP$ zHTBinzqb4K05kFCJTKz`_s;_ch_F-dJ6AS@;_(j3t`~B`u=RbK+Uyj&m4p5Smh~XO zhPH-&Ems2;F(9`RTzYKQFE{YF&@X<|gZ3a(BQvs$Z8!d0Ji{Du&}a6w^OMHX{`yn* zwjMI9nT29H4Y#Ay95T&!RCBuY+8}@LA$Mp>8FcWv7Px`VWh-+09;cG9qY35QeL;Lb z+hNuf>z<^`#I~*AP}~jZ)mqd1ldHdG5%p#Loo!{-v_BVH`+WE0gA1u_VwBF9)_{jk zw=2|X5g+r2$a$2&Q&CDkyrx}L6Ujk#3Ut=%q5dv^5wPS3ZeNH>3$Yf%=f-)?pYH^D zBgz`{^X{^4-E*inAvWtg%*bKw) z40H+&*dDT)qf@lUw|yU1R2 zzijKDVC%KsYbvw{9+I$cln#uMX*)`URm$cE95of#b?JF{=;^$)O>8N*cPAFk+YhvW=^Qtxqw#$$O0jIJaX|`oy(i)ZgsyG;DJXr4; zFixHM(ZBUdeokmG?NwL}=Qrq_tP@!sOFDV>SRBmV^a-~vh0vh>g`eha7FSXXmgD-N z5@^R*U-2+W+F1GN` z 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 getSplitColumns() { - return conf.getStringCollection(SPLIT_COLUMNS); - } - - public void setSplitColumns(Collection splitColumns) { - conf.setStrings(SPLIT_COLUMNS, splitColumns.toArray(new String[0])); - } - - public int getNumRowsPerQueryPart() { - return conf.getInt(NUM_ROWS_PER_QUERY_PART, 100000); - } - - public void setNumRowsPerQueryPart(int numRowsPerQueryPart) { - conf.setInt(NUM_ROWS_PER_QUERY_PART, numRowsPerQueryPart); - } - - public SplitQueryRequest.Algorithm getAlgorithm() { - return conf.getEnum(ALGORITHM, SplitQueryRequest.Algorithm.EQUAL_SPLITS); - } - - public void setAlgorithm(SplitQueryRequest.Algorithm algorithm) { - conf.setEnum(ALGORITHM, algorithm); - } - - public String getRpcFactoryClass() { - return conf.get(RPC_FACTORY_CLASS); - } - - public void setRpcFactoryClass(Class clz) { - conf.set(RPC_FACTORY_CLASS, clz.getName()); - } - - public Query.ExecuteOptions.IncludedFields getIncludedFields() { - return conf.getEnum(INCLUDED_FIELDS, Query.ExecuteOptions.IncludedFields.TYPE_AND_NAME); - } - - public void setIncludedFields(Query.ExecuteOptions.IncludedFields includedFields) { - conf.setEnum(INCLUDED_FIELDS, includedFields); - } -} diff --git a/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputFormat.java b/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputFormat.java deleted file mode 100644 index 5a9326980d8..00000000000 --- a/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputFormat.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2019 The Vitess Authors. - - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - - * http://www.apache.org/licenses/LICENSE-2.0 - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.vitess.hadoop; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.collect.Lists; - -import io.vitess.client.Context; -import io.vitess.client.RpcClient; -import io.vitess.client.RpcClientFactory; -import io.vitess.client.VTGateBlockingConn; -import io.vitess.proto.Query.SplitQueryRequest.Algorithm; -import io.vitess.proto.Vtgate.SplitQueryResponse; - -import org.apache.hadoop.io.NullWritable; -import org.apache.hadoop.mapreduce.InputFormat; -import org.apache.hadoop.mapreduce.InputSplit; -import org.apache.hadoop.mapreduce.Job; -import org.apache.hadoop.mapreduce.JobContext; -import org.apache.hadoop.mapreduce.RecordReader; -import org.apache.hadoop.mapreduce.TaskAttemptContext; - -import org.joda.time.Duration; - -import java.io.IOException; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Random; - -/** - * {@link VitessInputFormat} is the {@link org.apache.hadoop.mapreduce.InputFormat} for tables in - * Vitess. Input splits ({@link VitessInputSplit}) are fetched from VtGate via an RPC. map() calls - * are supplied with a {@link RowWritable}. - */ -public class VitessInputFormat extends InputFormat { - - @Override - public List getSplits(JobContext context) { - VitessConf conf = new VitessConf(context.getConfiguration()); - List splitResult; - try { - @SuppressWarnings("unchecked") - Class rpcFactoryClass = - (Class) Class.forName(conf.getRpcFactoryClass()); - List addressList = Arrays.asList(conf.getHosts().split(",")); - int index = new Random().nextInt(addressList.size()); - - RpcClient rpcClient = rpcFactoryClass.newInstance().create( - Context.getDefault().withDeadlineAfter(Duration.millis(conf.getTimeoutMs())), - addressList.get(index)); - - try (VTGateBlockingConn vtgate = new VTGateBlockingConn(rpcClient)) { - splitResult = vtgate.splitQuery( - Context.getDefault(), - conf.getKeyspace(), - conf.getInputQuery(), - null /* bind vars */, - conf.getSplitColumns(), - conf.getSplitCount(), - conf.getNumRowsPerQueryPart(), - conf.getAlgorithm()); - } - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SQLException - | IOException exc) { - throw new RuntimeException(exc); - } - - List splits = Lists.newArrayList(); - for (SplitQueryResponse.Part part : splitResult) { - splits.add(new VitessInputSplit(part)); - } - - for (InputSplit split : splits) { - ((VitessInputSplit) split).setLocations(conf.getHosts().split(VitessConf.HOSTS_DELIM)); - } - return splits; - } - - @Override - public RecordReader createRecordReader(InputSplit split, - TaskAttemptContext context) throws IOException, InterruptedException { - return new VitessRecordReader(); - } - - /** - * Sets the necessary configurations for Vitess table input source - */ - public static void setInput( - Job job, - String hosts, - String keyspace, - String query, - Collection splitColumns, - int splitCount, - int numRowsPerQueryPart, - Algorithm algorithm, - Class rpcFactoryClass) { - job.setInputFormatClass(VitessInputFormat.class); - VitessConf vtConf = new VitessConf(job.getConfiguration()); - vtConf.setHosts(checkNotNull(hosts)); - vtConf.setKeyspace(checkNotNull(keyspace)); - vtConf.setInputQuery(checkNotNull(query)); - vtConf.setSplitColumns(checkNotNull(splitColumns)); - vtConf.setSplitCount(splitCount); - vtConf.setNumRowsPerQueryPart(numRowsPerQueryPart); - vtConf.setAlgorithm(checkNotNull(algorithm)); - vtConf.setRpcFactoryClass(checkNotNull(rpcFactoryClass)); - } -} diff --git a/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputSplit.java b/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputSplit.java deleted file mode 100644 index 251bdf1f763..00000000000 --- a/java/hadoop/src/main/java/io/vitess/hadoop/VitessInputSplit.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2019 The Vitess Authors. - - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - - * http://www.apache.org/licenses/LICENSE-2.0 - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.vitess.hadoop; - -import com.google.common.io.BaseEncoding; - -import io.vitess.proto.Vtgate.SplitQueryResponse; - -import org.apache.hadoop.io.Writable; -import org.apache.hadoop.mapreduce.InputSplit; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -public class VitessInputSplit extends InputSplit implements Writable { - - private String[] locations; - private SplitQueryResponse.Part split; - - public VitessInputSplit(SplitQueryResponse.Part split) { - this.split = split; - } - - public VitessInputSplit() { - } - - public SplitQueryResponse.Part getSplit() { - return split; - } - - public void setLocations(String[] locations) { - this.locations = locations; - } - - @Override - public long getLength() throws IOException, InterruptedException { - return split.getSize(); - } - - @Override - public String[] getLocations() throws IOException, InterruptedException { - return locations; - } - - @Override - public void readFields(DataInput input) throws IOException { - split = SplitQueryResponse.Part.parseFrom(BaseEncoding.base64().decode(input.readUTF())); - } - - @Override - public void write(DataOutput output) throws IOException { - output.writeUTF(BaseEncoding.base64().encode(split.toByteArray())); - } -} diff --git a/java/hadoop/src/main/java/io/vitess/hadoop/VitessRecordReader.java b/java/hadoop/src/main/java/io/vitess/hadoop/VitessRecordReader.java deleted file mode 100644 index 0f649f281e6..00000000000 --- a/java/hadoop/src/main/java/io/vitess/hadoop/VitessRecordReader.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2019 The Vitess Authors. - - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - - * http://www.apache.org/licenses/LICENSE-2.0 - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.vitess.hadoop; - -import io.vitess.client.Context; -import io.vitess.client.RpcClient; -import io.vitess.client.RpcClientFactory; -import io.vitess.client.VTGateBlockingConn; -import io.vitess.client.cursor.Cursor; -import io.vitess.client.cursor.Row; -import io.vitess.proto.Query; -import io.vitess.proto.Query.BoundQuery; -import io.vitess.proto.Topodata.TabletType; -import io.vitess.proto.Vtgate.SplitQueryResponse; - -import org.apache.hadoop.io.NullWritable; -import org.apache.hadoop.mapreduce.InputSplit; -import org.apache.hadoop.mapreduce.RecordReader; -import org.apache.hadoop.mapreduce.TaskAttemptContext; - -import org.joda.time.Duration; - -import java.io.IOException; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.List; -import java.util.Random; - -public class VitessRecordReader extends RecordReader { - - private VitessInputSplit split; - private VTGateBlockingConn vtgate; - private VitessConf conf; - private long rowsProcessed = 0; - private Cursor cursor; - private RowWritable currentRow; - private Query.ExecuteOptions.IncludedFields includedFields; - - /** - * Fetch connection parameters from Configuration and open VtGate connection. - */ - @Override - public void initialize(InputSplit split, TaskAttemptContext context) - throws IOException, InterruptedException { - this.split = (VitessInputSplit) split; - conf = new VitessConf(context.getConfiguration()); - try { - @SuppressWarnings("unchecked") - Class rpcFactoryClass = - (Class) Class.forName(conf.getRpcFactoryClass()); - List addressList = Arrays.asList(conf.getHosts().split(",")); - int index = new Random().nextInt(addressList.size()); - - RpcClient rpcClient = rpcFactoryClass.newInstance().create( - Context.getDefault().withDeadlineAfter(Duration.millis(conf.getTimeoutMs())), - addressList.get(index)); - vtgate = new VTGateBlockingConn(rpcClient); - includedFields = conf.getIncludedFields(); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException exc) { - throw new RuntimeException(exc); - } - } - - @Override - public void close() throws IOException { - if (vtgate != null) { - try { - vtgate.close(); - vtgate = null; - } catch (IOException exc) { - throw new RuntimeException(exc); - } - } - } - - @Override - public NullWritable getCurrentKey() throws IOException, InterruptedException { - return NullWritable.get(); - } - - @Override - public RowWritable getCurrentValue() throws IOException, InterruptedException { - return currentRow; - } - - @Override - public float getProgress() throws IOException, InterruptedException { - if (rowsProcessed > split.getLength()) { - return 0.9f; - } - return rowsProcessed / split.getLength(); - } - - /** - * Fetches the next row. If this is the first invocation for the split, execute the streaming - * query. Subsequent calls just advance the iterator. - */ - @Override - public boolean nextKeyValue() throws IOException, InterruptedException { - try { - if (cursor == null) { - SplitQueryResponse.Part splitInfo = split.getSplit(); - if (splitInfo.hasKeyRangePart()) { - BoundQuery query = splitInfo.getQuery(); - SplitQueryResponse.KeyRangePart keyRangePart = splitInfo.getKeyRangePart(); - cursor = vtgate.streamExecuteKeyRanges(Context.getDefault(), query.getSql(), - keyRangePart.getKeyspace(), keyRangePart.getKeyRangesList(), query.getBindVariables(), - TabletType.RDONLY, includedFields); - } else if (splitInfo.hasShardPart()) { - BoundQuery query = splitInfo.getQuery(); - SplitQueryResponse.ShardPart shardPart = splitInfo.getShardPart(); - cursor = vtgate.streamExecuteShards(Context.getDefault(), query.getSql(), - shardPart.getKeyspace(), shardPart.getShardsList(), query.getBindVariables(), - TabletType.RDONLY, includedFields); - } else { - throw new IllegalArgumentException("unknown split info: " + splitInfo); - } - } - Row row = cursor.next(); - if (row == null) { - currentRow = null; - } else { - currentRow = new RowWritable(row); - } - } catch (SQLException exc) { - throw new RuntimeException(exc); - } - - if (currentRow == null) { - return false; - } - rowsProcessed++; - return true; - } -} diff --git a/java/hadoop/src/test/java/io/vitess/hadoop/MapReduceIT.java b/java/hadoop/src/test/java/io/vitess/hadoop/MapReduceIT.java deleted file mode 100644 index 01e20b6eaf4..00000000000 --- a/java/hadoop/src/test/java/io/vitess/hadoop/MapReduceIT.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2019 The Vitess Authors. - - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - - * http://www.apache.org/licenses/LICENSE-2.0 - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.vitess.hadoop; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import io.vitess.client.TestEnv; -import io.vitess.client.TestUtil; -import io.vitess.proto.Query.SplitQueryRequest.Algorithm; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.io.IntWritable; -import org.apache.hadoop.io.NullWritable; -import org.apache.hadoop.mapred.HadoopTestCase; -import org.apache.hadoop.mapreduce.Job; -import org.apache.hadoop.mapreduce.MapReduceTestUtil; -import org.apache.hadoop.mapreduce.Mapper; -import org.apache.hadoop.mapreduce.Reducer; -import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; -import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.sql.SQLException; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -import junit.extensions.TestSetup; -import junit.framework.TestSuite; -import vttest.Vttest.Keyspace; -import vttest.Vttest.Shard; -import vttest.Vttest.VTTestTopology; - - -/** - * Integration tests for MapReductions in Vitess. These tests use an in-process Hadoop cluster via - * {@link HadoopTestCase}. These tests are JUnit3 style because of this dependency. Vitess setup for - * these tests require at least one rdonly instance per shard. - */ -public class MapReduceIT extends HadoopTestCase { - - private static final int NUM_ROWS = 40; - - public static TestEnv testEnv = getTestEnv(); - - public MapReduceIT() throws IOException { - super(HadoopTestCase.LOCAL_MR, HadoopTestCase.LOCAL_FS, 1, 1); - } - - /** - * Run a mapper only MR job and verify all the rows in the source table were outputted into HDFS. - */ - public void testDumpTableToHDFS() throws Exception { - // Configurations for the job, output from mapper as Text - Configuration conf = createJobConf(); - Job job = Job.getInstance(conf); - job.setJobName("table"); - job.setJarByClass(VitessInputFormat.class); - job.setMapperClass(TableMapper.class); - VitessInputFormat.setInput( - job, - "localhost:" + testEnv.getPort(), - testEnv.getKeyspace(), - "select id, name, age from vtgate_test", - ImmutableList.of(), - 4 /* splitCount */, - 0 /* numRowsPerQueryPart */, - Algorithm.EQUAL_SPLITS, - TestUtil.getRpcClientFactory().getClass()); - job.setOutputKeyClass(NullWritable.class); - job.setOutputValueClass(RowWritable.class); - job.setOutputFormatClass(TextOutputFormat.class); - job.setNumReduceTasks(0); - - Path outDir = new Path(testEnv.getTestOutputPath(), "mrvitess/output"); - FileSystem fs = FileSystem.get(conf); - if (fs.exists(outDir)) { - fs.delete(outDir, true); - } - FileOutputFormat.setOutputPath(job, outDir); - - job.waitForCompletion(true); - assertTrue(job.isSuccessful()); - - String[] outputLines = MapReduceTestUtil.readOutput(outDir, conf).split("\n"); - // there should be one line per row in the source table - assertEquals(NUM_ROWS, outputLines.length); - Set actualAges = new HashSet<>(); - Set actualNames = new HashSet<>(); - - // Parse and verify we've gotten all the ages and rows. - Gson gson = new Gson(); - for (String line : outputLines) { - String[] parts = line.split("\t"); - actualAges.add(Long.valueOf(parts[0])); - - // Rows are written as JSON since this is TextOutputFormat. - String rowJson = parts[1]; - Type mapType = new TypeToken>() { - }.getType(); - @SuppressWarnings("unchecked") - Map map = (Map) gson.fromJson(rowJson, mapType); - actualNames.add(map.get("name")); - } - - Set expectedAges = new HashSet<>(); - Set expectedNames = new HashSet<>(); - for (long i = 1; i <= NUM_ROWS; i++) { - // Generate values that match TestUtil.insertRows(). - expectedAges.add(i % 10); - expectedNames.add("name_" + i); - } - assertEquals(expectedAges.size(), actualAges.size()); - assertTrue(actualAges.containsAll(expectedAges)); - assertEquals(NUM_ROWS, actualNames.size()); - assertTrue(actualNames.containsAll(expectedNames)); - } - - /** - * Map all rows and aggregate by age at the reducer. - */ - public void testReducerAggregateRows() throws Exception { - Configuration conf = createJobConf(); - - Job job = Job.getInstance(conf); - job.setJobName("table"); - job.setJarByClass(VitessInputFormat.class); - job.setMapperClass(TableMapper.class); - VitessInputFormat.setInput( - job, - "localhost:" + testEnv.getPort(), - testEnv.getKeyspace(), - "select id, name, age from vtgate_test", - ImmutableList.of(), - 1 /* splitCount */, - 0 /* numRowsPerQueryPart */, - Algorithm.EQUAL_SPLITS, - TestUtil.getRpcClientFactory().getClass()); - - job.setMapOutputKeyClass(IntWritable.class); - job.setMapOutputValueClass(RowWritable.class); - - job.setReducerClass(CountReducer.class); - job.setOutputKeyClass(NullWritable.class); - job.setOutputValueClass(IntWritable.class); - job.setOutputFormatClass(TextOutputFormat.class); - - Path outDir = new Path(testEnv.getTestOutputPath(), "mrvitess/output"); - FileSystem fs = FileSystem.get(conf); - if (fs.exists(outDir)) { - fs.delete(outDir, true); - } - FileOutputFormat.setOutputPath(job, outDir); - - job.waitForCompletion(true); - assertTrue(job.isSuccessful()); - - String[] outputLines = MapReduceTestUtil.readOutput(outDir, conf).split("\n"); - // There should be 10 different ages, because age = i % 10. - assertEquals(10, outputLines.length); - // All rows should be accounted for. - int totalRowsReduced = 0; - for (String line : outputLines) { - totalRowsReduced += Integer.parseInt(line); - } - assertEquals(NUM_ROWS, totalRowsReduced); - } - - public static class TableMapper - extends Mapper { - - @Override - public void map(NullWritable key, RowWritable value, Context context) - throws IOException, InterruptedException { - // Tag each record with its age. - try { - context.write(new IntWritable(value.get().getInt("age")), value); - } catch (SQLException e) { - throw new IOException(e); - } - } - } - - public static class CountReducer - extends Reducer { - - @Override - public void reduce(IntWritable key, Iterable values, Context context) - throws IOException, InterruptedException { - // Count how many records there are for each age. - int count = 0; - Iterator itr = values.iterator(); - while (itr.hasNext()) { - count++; - itr.next(); - } - context.write(NullWritable.get(), new IntWritable(count)); - } - } - - /** - * Create env with two shards each having a master, replica, and rdonly. - */ - static TestEnv getTestEnv() { - Keyspace keyspace = Keyspace.newBuilder().setName("test_keyspace") - .addShards(Shard.newBuilder().setName("-80").build()) - .addShards(Shard.newBuilder().setName("80-").build()).build(); - VTTestTopology topology = VTTestTopology.newBuilder().addKeyspaces(keyspace).build(); - TestEnv env = TestUtil.getTestEnv("test_keyspace", topology); - return env; - } - - public static TestSetup suite() { - return new TestSetup(new TestSuite(MapReduceIT.class)) { - - @Override - protected void setUp() throws Exception { - TestUtil.setupTestEnv(testEnv); - // Insert test rows - TestUtil.insertRows(testEnv, 1, NUM_ROWS); - } - - @Override - protected void tearDown() throws Exception { - TestUtil.teardownTestEnv(testEnv); - } - }; - - } -} diff --git a/java/pom.xml b/java/pom.xml index c433b863d81..b8ede24e018 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -24,7 +24,6 @@ client example grpc-client - hadoop jdbc diff --git a/py/vtdb/__init__.py b/py/vtdb/__init__.py deleted file mode 100644 index 278ebfd69c6..00000000000 --- a/py/vtdb/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -"""This file provides the PEP0249 compliant variables for this module. - -See https://www.python.org/dev/peps/pep-0249 for more information on these. -""" - -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Follows the Python Database API 2.0. -apilevel = '2.0' - -# Threads may share the module, but not connections. -# (we store session information in the connection now, that should be in the -# cursor but are not for historical reasons). -threadsafety = 2 - -# Named style, e.g. ...WHERE name=:name. -# -# Note we also provide a function in dbapi to convert from 'pyformat' -# to 'named', and prune unused bind variables in the SQL query. -# -# Also, we use an extension to bind variables to handle lists: -# Using the '::name' syntax (instead of ':name') will indicate a list bind -# variable. The type then has to be a list, set or tuple. -paramstyle = 'named' diff --git a/py/vtdb/prefer_vtroot_imports.py b/py/vtdb/prefer_vtroot_imports.py deleted file mode 100644 index 6456a7e8233..00000000000 --- a/py/vtdb/prefer_vtroot_imports.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Reorder sys.path to put $VTROOT/dist/* paths before others. - -This ensures libraries installed there will be preferred over other versions -that may be present at the system level. We do this at runtime because -regardless of what we set in the PYTHONPATH environment variable, the system -dist-packages folder gets prepended sometimes. - -To use this, just import it before importing packages that you want to make -sure are overridden from $VTROOT/dist. - -from vtdb import prefer_vtroot_imports # pylint: disable=unused-import -""" - -import os -import sys - - -def _prefer_vtroot_imports(): - """Reorder sys.path to put $VTROOT/dist before others.""" - - vtroot = os.environ.get('VTROOT') - if not vtroot: - # VTROOT is not set. Don't try anything. - return - dist = os.path.join(vtroot, 'dist') - - dist_paths = [] - other_paths = [] - - for path in sys.path: - if path: - if path.startswith(dist): - dist_paths.append(path) - else: - other_paths.append(path) - - sys.path = [''] + dist_paths + other_paths - -_prefer_vtroot_imports() diff --git a/py/vtproto/__init__.py b/py/vtproto/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/py/vtproto/vttest_pb2.py b/py/vtproto/vttest_pb2.py deleted file mode 100644 index 1c414f959bd..00000000000 --- a/py/vtproto/vttest_pb2.py +++ /dev/null @@ -1,206 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: vttest.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='vttest.proto', - package='vttest', - syntax='proto3', - serialized_options=_b('Z#vitess.io/vitess/go/vt/proto/vttest'), - serialized_pb=_b('\n\x0cvttest.proto\x12\x06vttest\"/\n\x05Shard\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10\x64\x62_name_override\x18\x02 \x01(\t\"\xb5\x01\n\x08Keyspace\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\x06shards\x18\x02 \x03(\x0b\x32\r.vttest.Shard\x12\x1c\n\x14sharding_column_name\x18\x03 \x01(\t\x12\x1c\n\x14sharding_column_type\x18\x04 \x01(\t\x12\x13\n\x0bserved_from\x18\x05 \x01(\t\x12\x15\n\rreplica_count\x18\x06 \x01(\x05\x12\x14\n\x0crdonly_count\x18\x07 \x01(\x05\"D\n\x0eVTTestTopology\x12#\n\tkeyspaces\x18\x01 \x03(\x0b\x32\x10.vttest.Keyspace\x12\r\n\x05\x63\x65lls\x18\x02 \x03(\tB%Z#vitess.io/vitess/go/vt/proto/vttestb\x06proto3') -) - - - - -_SHARD = _descriptor.Descriptor( - name='Shard', - full_name='vttest.Shard', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='vttest.Shard.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='db_name_override', full_name='vttest.Shard.db_name_override', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=24, - serialized_end=71, -) - - -_KEYSPACE = _descriptor.Descriptor( - name='Keyspace', - full_name='vttest.Keyspace', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='vttest.Keyspace.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='shards', full_name='vttest.Keyspace.shards', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='sharding_column_name', full_name='vttest.Keyspace.sharding_column_name', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='sharding_column_type', full_name='vttest.Keyspace.sharding_column_type', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='served_from', full_name='vttest.Keyspace.served_from', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='replica_count', full_name='vttest.Keyspace.replica_count', index=5, - number=6, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='rdonly_count', full_name='vttest.Keyspace.rdonly_count', index=6, - number=7, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=74, - serialized_end=255, -) - - -_VTTESTTOPOLOGY = _descriptor.Descriptor( - name='VTTestTopology', - full_name='vttest.VTTestTopology', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='keyspaces', full_name='vttest.VTTestTopology.keyspaces', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - _descriptor.FieldDescriptor( - name='cells', full_name='vttest.VTTestTopology.cells', index=1, - number=2, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=257, - serialized_end=325, -) - -_KEYSPACE.fields_by_name['shards'].message_type = _SHARD -_VTTESTTOPOLOGY.fields_by_name['keyspaces'].message_type = _KEYSPACE -DESCRIPTOR.message_types_by_name['Shard'] = _SHARD -DESCRIPTOR.message_types_by_name['Keyspace'] = _KEYSPACE -DESCRIPTOR.message_types_by_name['VTTestTopology'] = _VTTESTTOPOLOGY -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Shard = _reflection.GeneratedProtocolMessageType('Shard', (_message.Message,), dict( - DESCRIPTOR = _SHARD, - __module__ = 'vttest_pb2' - # @@protoc_insertion_point(class_scope:vttest.Shard) - )) -_sym_db.RegisterMessage(Shard) - -Keyspace = _reflection.GeneratedProtocolMessageType('Keyspace', (_message.Message,), dict( - DESCRIPTOR = _KEYSPACE, - __module__ = 'vttest_pb2' - # @@protoc_insertion_point(class_scope:vttest.Keyspace) - )) -_sym_db.RegisterMessage(Keyspace) - -VTTestTopology = _reflection.GeneratedProtocolMessageType('VTTestTopology', (_message.Message,), dict( - DESCRIPTOR = _VTTESTTOPOLOGY, - __module__ = 'vttest_pb2' - # @@protoc_insertion_point(class_scope:vttest.VTTestTopology) - )) -_sym_db.RegisterMessage(VTTestTopology) - - -DESCRIPTOR._options = None -# @@protoc_insertion_point(module_scope) diff --git a/py/vtproto/vttest_pb2_grpc.py b/py/vtproto/vttest_pb2_grpc.py deleted file mode 100644 index a89435267cb..00000000000 --- a/py/vtproto/vttest_pb2_grpc.py +++ /dev/null @@ -1,3 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -import grpc - diff --git a/py/vttest/__init__.py b/py/vttest/__init__.py deleted file mode 100644 index 4d32e37cccb..00000000000 --- a/py/vttest/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from vttest import environment -from vttest import mysql_db_mysqlctl - -environment.mysql_db_class = mysql_db_mysqlctl.MySqlDBMysqlctl diff --git a/py/vttest/environment.py b/py/vttest/environment.py deleted file mode 100644 index b84d14814de..00000000000 --- a/py/vttest/environment.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Contains environment specifications for vttest module. - -This module is meant to be overwritten upon import into a development -tree with the appropriate values. It works as is in the Vitess tree. -""" - -import os -import shutil -import tempfile - -# this is the location of the vtcombo binary -vtcombo_binary = os.path.join(os.environ['VTROOT'], 'bin', 'vtcombo') - -# this is the location of the mysqlctl binary, if mysql_db_mysqlctl is used. -mysqlctl_binary = os.path.join(os.environ['VTROOT'], 'bin', 'mysqlctl') - -# this is the base port set by options. -base_port = None - -# this is the class to use for MySqlDB instances -mysql_db_class = None - - -def get_test_directory(): - """Returns the toplevel directory for the tests. Might create it.""" - directory = tempfile.mkdtemp(prefix='vttest', - dir=os.environ.get('VTDATAROOT', None)) - # Override VTDATAROOT to point to the newly created dir - os.environ['VTDATAROOT'] = directory - os.mkdir(get_logs_directory(directory)) - return directory - - -def get_logs_directory(directory): - """Returns the directory for logs, might be based on directory. - - Args: - directory: the value returned by get_test_directory(). - Returns: - the directory for logs. - """ - return os.path.join(directory, 'logs') - - -def cleanup_test_directory(directory): - """Cleans up the test directory after the test is done. - - Args: - directory: the value returned by get_test_directory(). - """ - shutil.rmtree(directory) - - -def extra_vtcombo_parameters(): - """Returns extra parameters to send to vtcombo.""" - return [ - '-service_map', ','.join([ - 'grpc-vtgateservice', - 'grpc-vtctl', - ]), - ] - - -# pylint: disable=unused-argument -def process_is_healthy(name, addr): - - """Double-checks a process is healthy and ready for RPCs.""" - return True - - -def get_protocol(): - """Returns the protocol used between client and vtcombo.""" - return 'grpc' - - -def get_port(name, protocol=None): - """Returns the port to use for a given process. - - This is only called once per process, so picking an unused port will also - work. - - Args: - name: process name. - protocol: the protocol used. - - Returns: - the port to use. - - Raises: - ValueError: the port name is invalid. - """ - if name == 'vtcombo': - if protocol == 'grpc': - # We can't use the base_port for grpc. - return base_port + 1 - return base_port - elif name == 'mysql': - return base_port + 2 - elif name == 'vtcombo_mysql_port': - return base_port + 3 - else: - raise ValueError('name should be vtcombo or mysql, not %s' % name) diff --git a/py/vttest/init_data_options.py b/py/vttest/init_data_options.py deleted file mode 100644 index 04219f036b2..00000000000 --- a/py/vttest/init_data_options.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Stores options used for initializing the database with randomized data. - -The options stored correspond to command line flags. See run_local_database.py -for more details on each option. -""" - - -class InitDataOptions(object): - valid_attrs = set([ - 'rng_seed', - 'min_table_shard_size', - 'max_table_shard_size', - 'null_probability', - ]) - - def __setattr__(self, name, value): - if name not in self.valid_attrs: - raise Exception( - 'InitDataOptions: unsupported attribute: %s' % name) - self.__dict__[name] = value diff --git a/py/vttest/local_database.py b/py/vttest/local_database.py deleted file mode 100644 index 2634aef27a2..00000000000 --- a/py/vttest/local_database.py +++ /dev/null @@ -1,469 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Create a local Vitess database for testing.""" - -import glob -import logging -import os -import random -import re - -from vttest import environment -from vttest import vt_processes - - -class LocalDatabase(object): - """Set up a local Vitess database.""" - - def __init__(self, - topology, - schema_dir, - mysql_only, - init_data_options, - default_schema_dir=None, - extra_my_cnf=None, - snapshot_file=None, - charset='utf8', - mysql_server_bind_address=None): - """Initializes an object of this class. - - Args: - topology: a vttest.VTTestTopology object describing the topology. - schema_dir: see the documentation for the corresponding command line - flag in run_local_database.py - mysql_only: see the documentation for the corresponding command line - flag in run_local_database.py - init_data_options: an object of type InitDataOptions containing - options configuring populating the database with initial random data. - If the value is 'None' then the database will not be initialized - with random data. - default_schema_dir: a directory to use if no keyspace is found in the - schema_dir directory. - extra_my_cnf: additional cnf file to use for the EXTRA_MY_CNF var. - snapshot_file: A MySQL DB snapshot file. - charset: MySQL charset. - mysql_server_bind_address: MySQL server bind address. - """ - - self.topology = topology - self.schema_dir = schema_dir - self.mysql_only = mysql_only - self.init_data_options = init_data_options - self.default_schema_dir = default_schema_dir - self.extra_my_cnf = extra_my_cnf - self.snapshot_file = snapshot_file - self.charset = charset - self.mysql_server_bind_address = mysql_server_bind_address - - def setup(self): - """Create a MySQL instance and all Vitess processes.""" - mysql_port = environment.get_port('mysql') - self.directory = environment.get_test_directory() - self.mysql_db = environment.mysql_db_class( - self.directory, mysql_port, self.extra_my_cnf, self.snapshot_file) - - self.mysql_db.setup() - if not self.snapshot_file: - self.create_databases() - self.load_schema() - if self.init_data_options is not None: - self.rng = random.Random(self.init_data_options.rng_seed) - self.populate_with_random_data() - if self.mysql_only: - return - - vt_processes.start_vt_processes(self.directory, self.topology, - self.mysql_db, self.schema_dir, - charset=self.charset, mysql_server_bind_address=self.mysql_server_bind_address) - - def teardown(self): - """Kill all Vitess processes and wait for them to end. - - MySQLTestDB's wrapper script will take care of mysqld. - """ - if not self.mysql_only: - self.kill() - self.wait() - self.mysql_db.teardown() - environment.cleanup_test_directory(self.directory) - - def kill(self): - """Kill all Vitess processes.""" - vt_processes.kill_vt_processes() - - def wait(self): - """Wait for all Vitess processes to end.""" - vt_processes.wait_vt_processes() - - def vtgate_addr(self): - """Get the host:port for vtgate.""" - if environment.get_protocol() == 'grpc': - return vt_processes.vtcombo_process.grpc_addr() - return vt_processes.vtcombo_process.addr() - - def config(self): - """Returns a dict with enough information to be able to connect.""" - if self.mysql_only: - return self.mysql_db.config() - - result = { - 'port': vt_processes.vtcombo_process.port, - 'socket': self.mysql_db.unix_socket(), - 'vtcombo_mysql_port': vt_processes.vtcombo_process.vtcombo_mysql_port, - } - - if environment.get_protocol() == 'grpc': - result['grpc_port'] = vt_processes.vtcombo_process.grpc_port - return result - - def mysql_execute(self, queries, db_name=''): - """Execute queries directly on MySQL. - - The queries will be executed in a single transaction. - - Args: - queries: A list of strings. The SQL statements to execute. - db_name: The database name to use. - - Returns: - The results of the last query as a list of row tuples. - """ - conn = self.mysql_db.connect(db_name) - cursor = conn.cursor() - - for query in queries: - cursor.execute(query) - result = cursor.fetchall() - - cursor.close() - # Commit all of the queries. - conn.commit() - conn.close() - return result - - def create_databases(self): - """Create a database for each shard.""" - - cmds = [] - for kpb in self.topology.keyspaces: - if kpb.served_from: - # redirected keyspaces have no underlying database - continue - - for spb in kpb.shards: - db_name = spb.db_name_override - if not db_name: - db_name = 'vt_%s_%s' % (kpb.name, spb.name) - cmds.append('create database `%s`' % db_name) - logging.info('Creating databases') - self.mysql_execute(cmds) - - def load_schema(self): - """Load schema SQL from data files.""" - - if not self.schema_dir: - return - - if not os.path.isdir(self.schema_dir): - raise Exception('schema_dir "%s" is not a directory.' % self.schema_dir) - - for kpb in self.topology.keyspaces: - if kpb.served_from: - # redirected keyspaces have no underlying database - continue - - keyspace = kpb.name - keyspace_dir = os.path.join(self.schema_dir, keyspace) - schema_dir = keyspace_dir - if not os.path.isdir(schema_dir): - schema_dir = self.default_schema_dir - if not schema_dir or not os.path.isdir(schema_dir): - raise Exception( - 'No subdirectory found in schema dir %s for keyspace %s. ' - 'No valid default_schema_dir (set to %s) was found. ' - 'For keyspaces without an initial schema, create the ' - 'directory %s and leave a README file to explain why the ' - 'directory exists. ' - 'Alternatively, disable loading schemas by setting --schema_dir ' - 'to "" or set --default_schema_dir to a valid schema.' % - (self.schema_dir, keyspace, self.default_schema_dir, - keyspace_dir)) - - for filepath in glob.glob(os.path.join(schema_dir, '*.sql')): - logging.info('Loading schema for keyspace %s from file %s', - keyspace, filepath) - cmds = self.get_sql_commands_from_file(filepath, schema_dir) - - # Run the cmds on each shard and cell in the keyspace. - for spb in kpb.shards: - db_name = spb.db_name_override - if not db_name: - db_name = 'vt_%s_%s' % (kpb.name, spb.name) - self.mysql_execute(cmds, db_name=db_name) - - def populate_with_random_data(self): - """Populates all shards with randomly generated data.""" - - for kpb in self.topology.keyspaces: - if kpb.served_from: - # redirected keyspaces have no underlying database - continue - - for spb in kpb.shards: - db_name = spb.db_name_override - if not db_name: - db_name = 'vt_%s_%s' % (kpb.name, spb.name) - self.populate_shard_with_random_data(db_name) - - def populate_shard_with_random_data(self, db_name): - """Populates the given database with randomly generated data. - - Every table in the database is populated. - - Args: - db_name: The shard database name (string). - """ - - tables = self.mysql_execute(['SHOW TABLES'], db_name) - for table in tables: - self.populate_table_with_random_data(db_name, table[0]) - - # The number of rows inserted in a single INSERT statement. - batch_insert_size = 1000 - - def populate_table_with_random_data(self, db_name, table_name): - """Populates the given table with randomly generated data. - - Queries the database for the table schema and then populates - the columns with randomly generated data. - - Args: - db_name: The shard database name (string). - table_name: The name of the table to populate (string). - """ - - field_infos = self.mysql_execute(['DESCRIBE %s' % table_name], db_name) - num_rows = self.rng.randint(self.init_data_options.min_table_shard_size, - self.init_data_options.max_table_shard_size) - rows = [] - for _ in xrange(num_rows): - row = [] - for field_info in field_infos: - field_type = field_info[1] - field_allow_nulls = (field_info[2] == 'YES') - row.append( - self.generate_random_field( - table_name, field_type, field_allow_nulls)) - rows.append(row) - - # Insert 'rows' into the database in batches of size - # self.batch_insert_size - field_names = [field_info[0] for field_info in field_infos] - for index in xrange(0, len(rows), self.batch_insert_size): - self.batch_insert(db_name, - table_name, - field_names, - rows[index:index + self.batch_insert_size]) - - def batch_insert(self, db_name, table_name, field_names, rows): - """Inserts the rows in 'rows' into 'table_name' of database 'db_name'. - - Args: - db_name: The name of the database containing the table. - table_name: The name of the table to populate. - field_names: The list of the field names in the table. - rows: A list of tuples with each tuple containing - the string representations of the fields. - The order of the representation must match the order of the field - names listed in 'field_names'. - """ - - field_names_string = ','.join(field_names) - values_string = ','.join(['(' + ','.join(row) +')' for row in rows]) - # We use "INSERT IGNORE" to ignore duplicate key errors. - insert_query = ('INSERT IGNORE INTO %s (%s) VALUES %s' % - (table_name, field_names_string, values_string)) - logging.info('Executing in database %s: %s', db_name, insert_query) - self.mysql_execute([insert_query], db_name) - - def generate_random_field(self, table_name, field_type, field_allows_nulls): - """Generates a random field string representation. - - By 'string representation' we mean a string that is suitable to be a part - of an 'INSERT INTO' SQL statement. - - Args: - table_name: The name of the table that will contain the generated field - value. Only used for a descriptive exception message in case of - an error. - field_type: The field_type as given by a "DESCRIBE " SQL statement. - field_allows_nulls: Should be 'true' if this field allows NULLS. - - Returns: - The random field. - - Raises: - Exception: If 'field_type' is not supported. - """ - - value = None - if field_type.startswith('tinyint'): - value = self.random_integer(field_type, 1) - elif field_type.startswith('smallint'): - value = self.random_integer(field_type, 2) - elif field_type.startswith('mediumint'): - value = self.random_integer(field_type, 3) - elif field_type.startswith('int'): - value = self.random_integer(field_type, 4) - elif field_type.startswith('bigint'): - value = self.random_integer(field_type, 8) - elif field_type.startswith('decimal'): - value = self.random_decimal(field_type) - else: - raise Exception('Populating random data in field type: %s is not yet ' - 'supported. (table: %s)' % (field_type, table_name)) - if (field_allows_nulls and - self.true_with_probability(self.init_data_options.null_probability)): - return 'NULL' - return value - - def true_with_probability(self, true_probability): - """Returns a pseudo-random boolean. - - Args: - true_probability: The probability to use for returning 'true'. - Returns: - The value 'true' is with probability 'true_probability'. - """ - - return self.rng.uniform(0, 1) < true_probability - - def random_integer(self, field_type, num_bytes): - num_bits = 8*num_bytes - if field_type.endswith('unsigned'): - return '%d' % (self.rng.randint(0, 2**num_bits-1)) - return '%d' % (self.rng.randint(-2**(num_bits-1), 2**(num_bits-1)-1)) - - decimal_regexp = re.compile(r'decimal\((\d+),(\d+)\)') - - def random_decimal(self, field_type): - match = self.decimal_regexp.match(field_type) - if match is None: - raise Exception("Can't parse 'decimal' field type: %s" % field_type) - num_digits_right = int(match.group(2)) - num_digits_left = int(match.group(1))-num_digits_right - boundary = 10**num_digits_left-1 - rand = self.rng.uniform(-boundary, boundary) - return '%.*f' % (num_digits_right, rand) - - def get_sql_commands_from_file(self, filename, source_root=None): - """Given a file, extract an array of commands from the file. - - Automatically strips out three types of MySQL comment syntax: - '--' at beginning of line: line removed - '-- ': remove everything from here to line's end (note space after dashes) - '#': remove everything from here to line's end - MySQL's handling of C-style /* ... */ comments is weird, so we - leave them alone for now. See the MySQL manual 6.1.6 "Comment Syntax" - for all the weird complications. - - Args: - filename: the SQL source file to use. - source_root: if specified, 'source FILENAME' lines in the SQL file will - source the specified filename relative to source_root. - - Returns: - A list of SQL commands. - """ - fd = open(filename) - lines = fd.readlines() - - inside_single_quotes = 0 - inside_double_quotes = 0 - commands = [] - cmd = '' - for line in lines: - # Strip newline and other trailing whitespace - line = line.rstrip() - - if (not inside_single_quotes and not inside_double_quotes and - line.startswith('--')): - # Line starts with '--', skip line - continue - - i = 0 - next_i = 0 - # Iterate through line, looking for special delimiters - while 1: - i = next_i - if i >= len(line): - break - - # By default, move to next character after this one - next_i = i + 1 - - if line[i] == '\\': - # Next character is literal, skip this and the next character - next_i = i + 2 - - elif line[i] == "'": - if not inside_double_quotes: - inside_single_quotes = not inside_single_quotes - - elif line[i] == '"': - if not inside_single_quotes: - inside_double_quotes = not inside_double_quotes - - elif not inside_single_quotes and not inside_double_quotes: - if line[i] == '#' or line[i:i+3] == '-- ': - # Found unquoted "#" or "-- ", ignore rest of line - line = line[:i] - break - - if line[i] == ';': - # Unquoted semicolon marks end of command - cmd += line[:i] - commands.append(cmd) - cmd = '' - - # Chop off everything before and including the semicolon - line = line[i+1:] - - # Start over at beginning of line - next_i = 0 - - # Reached end of line - if line and not line.isspace(): - if source_root and not cmd and line.startswith('source '): - commands.extend(self.get_sql_commands_from_file( - os.path.join(source_root, line[7:]), - source_root=source_root)) - else: - cmd += line - cmd += '\n' - - # Accept last command even if it doesn't end in semicolon - cmd = cmd.strip() - if cmd: - commands.append(cmd) - - return commands - - def __enter__(self): - self.setup() - return self - - def __exit__(self, exc_type, exc_info, tb): - self.teardown() diff --git a/py/vttest/mysql_db.py b/py/vttest/mysql_db.py deleted file mode 100644 index fe0c4e2037b..00000000000 --- a/py/vttest/mysql_db.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module defines the interface for the MySQL database. -""" - - -class MySqlDB(object): - """A MySqlDB contains basic info about a MySQL instance.""" - - def __init__(self, directory, port, extra_my_cnf=None, snapshot_file=None): - self._directory = directory - self._port = port - self._extra_my_cnf = extra_my_cnf - self._snapshot_file = snapshot_file - - def setup(self, port): - """Starts the MySQL database.""" - raise NotImplementedError('MySqlDB is the base class.') - - def teardown(self): - """Stops the MySQL database.""" - raise NotImplementedError('MySqlDB is the base class.') - - def username(self): - raise NotImplementedError('MySqlDB is the base class.') - - def password(self): - raise NotImplementedError('MySqlDB is the base class.') - - def hostname(self): - raise NotImplementedError('MySqlDB is the base class.') - - def port(self): - raise NotImplementedError('MySqlDB is the base class.') - - def unix_socket(self): - raise NotImplementedError('MySqlDB is the base class.') - - def config(self): - """Returns the json config to output.""" - raise NotImplementedError('MySqlDB is the base class.') diff --git a/py/vttest/mysql_db_mysqlctl.py b/py/vttest/mysql_db_mysqlctl.py deleted file mode 100644 index c1ecc8d049f..00000000000 --- a/py/vttest/mysql_db_mysqlctl.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module defines a mysqlctl based MySQL database. -""" - -import os -import subprocess - -import MySQLdb - -from vttest import environment -from vttest import mysql_db -from vttest.mysql_flavor import mysql_flavor - - -class MySqlDBMysqlctl(mysql_db.MySqlDB): - """Contains data and methods to manage a MySQL instance using mysqlctl.""" - - def __init__(self, directory, port, extra_my_cnf, snapshot_file=None): - super(MySqlDBMysqlctl, self).__init__( - directory, port, extra_my_cnf, snapshot_file) - - def setup(self): - cmd = [ - environment.mysqlctl_binary, - '-alsologtostderr', - '-tablet_uid', '1', - '-mysql_port', str(self._port), - 'init', - '-init_db_sql_file', - os.path.join(os.environ['VTROOT'], 'config/init_db.sql'), - ] - env = os.environ - env['VTDATAROOT'] = self._directory - my_cnf = mysql_flavor().my_cnf() - if self._extra_my_cnf: - my_cnf += ':%s' % self._extra_my_cnf - env['EXTRA_MY_CNF'] = my_cnf - result = subprocess.call(cmd, env=env) - if result != 0: - raise Exception('mysqlctl failed', result) - - def teardown(self): - cmd = [ - environment.mysqlctl_binary, - '-alsologtostderr', - '-tablet_uid', '1', - '-mysql_port', str(self._port), - 'shutdown', - ] - result = subprocess.call(cmd) - if result != 0: - raise Exception('mysqlctl failed', result) - - def connect(self, db_name): - return MySQLdb.connect(user='vt_dba', - unix_socket=self.unix_socket(), - db=db_name) - - def username(self): - return 'vt_dba' - - def password(self): - return '' - - def hostname(self): - return '' - - def port(self): - return self._port - - def unix_socket(self): - return os.path.join(self._directory, 'vt_0000000001', 'mysql.sock') - - def config(self): - return { - 'username': self.username(), - 'password': self.password(), - 'socket': self.unix_socket(), - } diff --git a/py/vttest/mysql_flavor.py b/py/vttest/mysql_flavor.py deleted file mode 100644 index c28bf979e02..00000000000 --- a/py/vttest/mysql_flavor.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Define abstractions for various mysql flavors. - -This module is used by mysql_db_mysqlctl.py to handle differences -between various flavors of mysql. -""" - -import logging -import os -import sys - - -# For now, vtroot is only used in this module. If other people -# need this, we should move it to environment. -if "VTROOT" not in os.environ: - sys.stderr.write( - "ERROR: Vitess environment not set up. " - 'Please run "source dev.env" first.\n') - sys.exit(1) - -vtroot = os.environ["VTROOT"] - -class MysqlFlavor(object): - """Base class with default SQL statements.""" - - def my_cnf(self): - """Returns the path to an extra my_cnf file, or None.""" - return None - - -class MariaDB(MysqlFlavor): - """Overrides specific to MariaDB.""" - - def my_cnf(self): - files = [ - os.path.join(vtroot, "config/mycnf/default-fast.cnf"), - ] - return ":".join(files) - -class MariaDB103(MysqlFlavor): - """Overrides specific to MariaDB 10.3""" - - def my_cnf(self): - files = [ - os.path.join(vtroot, "config/mycnf/default-fast.cnf"), - ] - return ":".join(files) - -class MySQL56(MysqlFlavor): - """Overrides specific to MySQL 5.6.""" - - def my_cnf(self): - files = [ - os.path.join(vtroot, "config/mycnf/default-fast.cnf"), - ] - return ":".join(files) - -class MySQL80(MysqlFlavor): - """Overrides specific to MySQL 8.0.""" - - def my_cnf(self): - files = [ - os.path.join(vtroot, "config/mycnf/default-fast.cnf"), - ] - return ":".join(files) - -__mysql_flavor = None - - -# mysql_flavor is a function because we need something to import before the -# actual __mysql_flavor is initialized, since that doesn't happen until after -# the command-line options are parsed. If we make mysql_flavor a variable and -# import it before it's initialized, the module that imported it won't get the -# updated value when it's later initialized. -def mysql_flavor(): - return __mysql_flavor - - -def set_mysql_flavor(flavor): - global __mysql_flavor - - # Last default is there because the environment variable might be set to "". - flavor = flavor or os.environ.get("MYSQL_FLAVOR", "MySQL56") or "MySQL56" - - # Set the environment variable explicitly in case we're overriding it via - # command-line flag. - os.environ["MYSQL_FLAVOR"] = flavor - - if flavor == "MariaDB": - __mysql_flavor = MariaDB() - elif flavor == "MariaDB103": - __mysql_flavor = MariaDB103() - elif flavor == "MySQL80": - __mysql_flavor = MySQL80() - elif flavor == "MySQL56": - __mysql_flavor = MySQL56() - else: - logging.error("Unknown MYSQL_FLAVOR '%s'", flavor) - exit(1) - - logging.debug("Using MYSQL_FLAVOR=%s", str(flavor)) diff --git a/py/vttest/run_local_database.py b/py/vttest/run_local_database.py deleted file mode 100755 index ba49b6546c1..00000000000 --- a/py/vttest/run_local_database.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -r"""Command-line tool for starting a local Vitess database for testing. - -USAGE: - - $ run_local_database --port 12345 \ - --proto_topo \ - --schema_dir /path/to/schema/dir - -It will run the tool, logging to stderr. On stdout, a small json structure -can be waited on and then parsed by the caller to figure out how to reach -the vtgate process. - -As an alternative to using proto_topo, a local instance can be started by using -additional flags, such as: - - $ run_local_database --port 12345 \ - --schema_dir /path/to/schema/dir \ - --cells cell1,cell2 --keyspaces ks1,ks2 \ - --num_shards 1,2 - -This will create an instance with two keyspaces in two cells, one with a single -shard and another with two shards. - -Once done with the test, send an empty line to this process for it to clean-up, -and then just wait for it to exit. - -""" - -import json -import logging -import optparse -import os -import sys - - -from google.protobuf import text_format - -from vtproto import vttest_pb2 -from vtdb import prefer_vtroot_imports # pylint: disable=unused-import -from vttest import environment -from vttest import init_data_options -from vttest import local_database -from vttest import mysql_flavor -from vttest import sharding_utils - - -def main(cmdline_options): - topology = vttest_pb2.VTTestTopology() - if cmdline_options.proto_topo: - # Text-encoded proto topology object, just parse it. - topology = text_format.Parse(cmdline_options.proto_topo, topology) - if not topology.cells: - topology.cells.append('test') - else: - cells = [] - keyspaces = [] - shard_counts = [] - if cmdline_options.cells: - cells = cmdline_options.cells.split(',') - if cmdline_options.keyspaces: - keyspaces = cmdline_options.keyspaces.split(',') - if cmdline_options.num_shards: - shard_counts = [int(x) for x in cmdline_options.num_shards.split(',')] - - for cell in cells: - topology.cells.append(cell) - for keyspace, num_shards in zip(keyspaces, shard_counts): - ks = topology.keyspaces.add(name=keyspace) - for shard in sharding_utils.get_shard_names(num_shards): - ks.shards.add(name=shard) - ks.replica_count = cmdline_options.replica_count - ks.rdonly_count = cmdline_options.rdonly_count - - environment.base_port = cmdline_options.port - - init_data_opts = None - if cmdline_options.initialize_with_random_data: - init_data_opts = init_data_options.InitDataOptions() - init_data_opts.rng_seed = cmdline_options.rng_seed - init_data_opts.min_table_shard_size = cmdline_options.min_table_shard_size - init_data_opts.max_table_shard_size = cmdline_options.max_table_shard_size - init_data_opts.null_probability = cmdline_options.null_probability - - extra_my_cnf = '' - if cmdline_options.extra_my_cnf: - extra_my_cnf += ':' + cmdline_options.extra_my_cnf - - with local_database.LocalDatabase( - topology, - cmdline_options.schema_dir, - cmdline_options.mysql_only, - init_data_opts, - default_schema_dir=cmdline_options.default_schema_dir, - extra_my_cnf=extra_my_cnf, - charset=cmdline_options.charset, - snapshot_file=cmdline_options.snapshot_file, - mysql_server_bind_address=cmdline_options.mysql_server_bind_address) as local_db: - print json.dumps(local_db.config()) - sys.stdout.flush() - try: - raw_input() - except EOFError: - sys.stderr.write( - 'WARNING: %s: No empty line was received on stdin.' - ' Instead, stdin was closed and the cluster will be shut down now.' - ' Make sure to send the empty line instead to proactively shutdown' - ' the local cluster. For example, did you forget the shutdown in' - ' your test\'s tearDown()?\n' % os.path.basename(__file__)) - -if __name__ == '__main__': - - parser = optparse.OptionParser() - parser.add_option( - '-p', '--port', type='int', - help='Port to use for vtcombo. If this is 0, a random port ' - 'will be chosen.') - parser.add_option( - '-o', '--proto_topo', - help='Define the fake cluster topology as a compact text format encoded' - ' vttest proto. See vttest.proto for more information.') - parser.add_option( - '-s', '--schema_dir', - help='Directory for initial schema files. Within this dir,' - ' there should be a subdir for each keyspace. Within' - ' each keyspace dir, each file is executed as SQL' - ' after the database is created on each shard.' - ' If the directory contains a vschema.json file, it' - ' will be used as the vschema for the V3 API.') - parser.add_option( - '-e', '--default_schema_dir', - help='Default directory for initial schema files. If no schema is found' - ' in schema_dir, default to this location.') - parser.add_option( - '-m', '--mysql_only', action='store_true', - help='If this flag is set only mysql is initialized.' - ' The rest of the vitess components are not started.' - ' Also, the output specifies the mysql unix socket' - ' instead of the vtgate port.') - parser.add_option( - '-r', '--initialize_with_random_data', action='store_true', - help='If this flag is each table-shard will be initialized' - ' with random data. See also the "rng_seed" and "min_shard_size"' - ' and "max_shard_size" flags.') - parser.add_option( - '-d', '--rng_seed', type='int', default=123, - help='The random number generator seed to use when initializing' - ' with random data (see also --initialize_with_random_data).' - ' Multiple runs with the same seed will result with the same' - ' initial data.') - parser.add_option( - '-x', '--min_table_shard_size', type='int', default=1000, - help='The minimum number of initial rows in a table shard. Ignored if' - '--initialize_with_random_data is false. The actual number is chosen' - ' randomly.') - parser.add_option( - '-y', '--max_table_shard_size', type='int', default=10000, - help='The maximum number of initial rows in a table shard. Ignored if' - '--initialize_with_random_data is false. The actual number is chosen' - ' randomly') - parser.add_option( - '-n', '--null_probability', type='float', default=0.1, - help='The probability to initialize a field with "NULL" ' - ' if --initialize_with_random_data is true. Only applies to fields' - ' that can contain NULL values.') - parser.add_option( - '-f', '--extra_my_cnf', - help='extra files to add to the config, separated by ":"') - parser.add_option( - '--mysql_server_bind_address', - help='mysql server bind address ":"') - parser.add_option( - '-v', '--verbose', action='store_true', - help='Display extra error messages.') - parser.add_option('-c', '--cells', default='test', - help='Comma separated list of cells') - parser.add_option('-k', '--keyspaces', default='test_keyspace', - help='Comma separated list of keyspaces') - parser.add_option('--num_shards', default='2', - help='Comma separated shard count (one per keyspace)') - parser.add_option('--replica_count', type='int', default=2, - help='Replica tablets per shard (includes master)') - parser.add_option('--rdonly_count', type='int', default=1, - help='Rdonly tablets per shard') - parser.add_option('--charset', default='utf8', help='MySQL charset') - parser.add_option( - '--snapshot_file', default=None, help='A MySQL DB snapshot file') - (options, args) = parser.parse_args() - if options.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # This will set the flavor based on the MYSQL_FLAVOR env var, - # or default to MariaDB. - mysql_flavor.set_mysql_flavor(None) - - main(options) diff --git a/py/vttest/sharding_utils.py b/py/vttest/sharding_utils.py deleted file mode 100644 index db9b15ff6b9..00000000000 --- a/py/vttest/sharding_utils.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -"""Sharding utils.""" - - -def get_shard_index(shard_name): - """Returns tuple of shard index, num_shards based on a shard name.""" - if shard_name in ['0', '-']: - return 0, 1 - - shard_begin, shard_end = shard_name.split('-') - num_bytes_used = max(len(shard_begin), len(shard_end)) / 2 - if shard_begin: - shard_begin = int(shard_begin, 16) - else: - shard_begin = 0 - if shard_end: - shard_end = int(shard_end, 16) - else: - shard_end = 1 << num_bytes_used * 8 - shard_width = shard_end - shard_begin - num_shards = (1 << num_bytes_used * 8) / (shard_width) - shard_num = shard_begin / shard_width - return shard_num, num_shards - - -def get_shard_name(shard, num_shards): - """Returns an appropriate shard name, as a string. - - A single shard name is simply 0; otherwise it will attempt to split up 0x100 - into multiple shards. For example, in a two sharded keyspace, shard 0 is - -80, shard 1 is 80-. This function currently only applies to sharding setups - where the shard count is 256 or less, and all shards are equal width. - - Args: - shard: The integer shard index (zero based) - num_shards: Total number of shards (int) - - Returns: - The shard name as a string. - """ - - if num_shards == 1: - return '0' - - shard_width = int(0x100 / num_shards) - - if shard == 0: - return '-%02x' % shard_width - elif shard == num_shards - 1: - return '%02x-' % (shard * shard_width) - else: - return '%02x-%02x' % (shard * shard_width, (shard + 1) * shard_width) - - -def get_shard_names(num_shards): - """Create a generator of shard names. - - Args: - num_shards: Total number of shards (int) - - Returns: - The shard name generator. - """ - return (get_shard_name(x, num_shards) for x in range(num_shards)) diff --git a/py/vttest/vt_processes.py b/py/vttest/vt_processes.py deleted file mode 100644 index 8530c2d5ee2..00000000000 --- a/py/vttest/vt_processes.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2019 The Vitess Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Starts the vtcombo process.""" - -import json -import logging -import os -import socket -import subprocess -import time -import urllib - -from google.protobuf import text_format - -from vttest import environment - - -class VtProcess(object): - """Base class for a vt process, vtcombo only now.""" - - START_RETRIES = 5 - - def __init__(self, name, directory, binary, port_name): - self.name = name - self.directory = directory - self.binary = binary - self.extraparams = [] - self.port_name = port_name - self.process = None - - def wait_start(self): - """Start the process and wait for it to respond on HTTP.""" - - for _ in xrange(0, self.START_RETRIES): - self.port = environment.get_port(self.port_name) - if environment.get_protocol() == 'grpc': - self.grpc_port = environment.get_port(self.port_name, protocol='grpc') - else: - self.grpc_port = None - logs_subdirectory = environment.get_logs_directory(self.directory) - cmd = [ - self.binary, - '-port', '%u' % self.port, - '-log_dir', logs_subdirectory, - '-alsologtostderr', - ] - if environment.get_protocol() == 'grpc': - cmd.extend(['-grpc_port', '%u' % self.grpc_port]) - cmd.extend(self.extraparams) - logging.info('Starting process: %s', cmd) - stdout = os.path.join(logs_subdirectory, '%s.%d.log' % - (self.name, self.port)) - self.stdout = open(stdout, 'w') - self.process = subprocess.Popen(cmd, - stdout=self.stdout) - timeout = time.time() + 60.0 - while time.time() < timeout: - if environment.process_is_healthy( - self.name, self.addr()) and self.get_vars(): - logging.info('%s started.', self.name) - return - elif self.process.poll() is not None: - logging.error('%s process exited prematurely.', self.name) - break - time.sleep(0.3) - - logging.error('cannot start %s process on time: %s ', - self.name, socket.getfqdn()) - self.kill() - - raise Exception('Failed %d times to run %s' % ( - self.START_RETRIES, - self.name)) - - def addr(self): - """Return the host:port of the process.""" - return '%s:%u' % (socket.getfqdn(), self.port) - - def grpc_addr(self): - """Get the grpc address of the process. - - Returns: - the grpc host:port of the process. - Only call this is environment.get_protocol() == 'grpc'. - """ - return '%s:%u' % (socket.getfqdn(), self.grpc_port) - - def get_vars(self): - """Return the debug vars.""" - data = None - try: - url = 'http://%s/debug/vars' % self.addr() - f = urllib.urlopen(url) - data = f.read() - f.close() - except IOError: - return None - try: - return json.loads(data) - except ValueError: - logging.error('%s', data) - raise - - def kill(self): - """Kill the process.""" - # These will proceed without error even if the process is already gone. - self.process.terminate() - - def wait(self): - """Wait for the process to end.""" - self.process.wait() - - -class VtcomboProcess(VtProcess): - """Represents a vtcombo subprocess.""" - - QUERYSERVER_PARAMETERS = [ - '-queryserver-config-pool-size', '4', - '-queryserver-config-query-timeout', '300', - '-queryserver-config-schema-reload-time', '60', - '-queryserver-config-stream-pool-size', '4', - '-queryserver-config-transaction-cap', '4', - '-queryserver-config-transaction-timeout', '300', - '-queryserver-config-txpool-timeout', '300', - ] - - def __init__(self, directory, topology, mysql_db, schema_dir, charset, - mysql_server_bind_address=None): - VtProcess.__init__(self, 'vtcombo-%s' % os.environ['USER'], directory, - environment.vtcombo_binary, port_name='vtcombo') - self.extraparams = [ - '-db_charset', charset, - '-db_app_user', mysql_db.username(), - '-db_app_password', mysql_db.password(), - '-db_dba_user', mysql_db.username(), - '-db_dba_password', mysql_db.password(), - '-proto_topo', text_format.MessageToString(topology, as_one_line=True), - '-mycnf_server_id', '1', - '-mycnf_socket_file', mysql_db.unix_socket(), - '-normalize_queries', - ] + self.QUERYSERVER_PARAMETERS + environment.extra_vtcombo_parameters() - if schema_dir: - self.extraparams.extend(['-schema_dir', schema_dir]) - if mysql_db.unix_socket(): - self.extraparams.extend(['-db_socket', mysql_db.unix_socket()]) - else: - self.extraparams.extend( - ['-db_host', mysql_db.hostname(), - '-db_port', str(mysql_db.port())]) - self.vtcombo_mysql_port = environment.get_port('vtcombo_mysql_port') - if mysql_server_bind_address: - # Binding to 0.0.0.0 instead of localhost makes it possible to connect to vtgate from outside a docker container - self.extraparams.extend(['-mysql_server_bind_address', mysql_server_bind_address]) - else: - self.extraparams.extend(['-mysql_server_bind_address', 'localhost']) - self.extraparams.extend( - ['-mysql_auth_server_impl', 'none', - '-mysql_server_port', str(self.vtcombo_mysql_port)]) - - -vtcombo_process = None - - -def start_vt_processes(directory, topology, mysql_db, schema_dir, - charset='utf8', mysql_server_bind_address=None): - """Start the vt processes. - - Args: - directory: the toplevel directory for the processes (logs, ...) - topology: a vttest.VTTestTopology object. - mysql_db: an instance of the mysql_db.MySqlDB class. - schema_dir: the directory that contains the schema / vschema. - charset: the character set for the database connections. - mysql_server_bind_address: MySQL server bind address for vtcombo. - """ - global vtcombo_process - - logging.info('start_vt_processes(directory=%s,vtcombo_binary=%s)', - directory, environment.vtcombo_binary) - vtcombo_process = VtcomboProcess(directory, topology, mysql_db, schema_dir, - charset, mysql_server_bind_address=mysql_server_bind_address) - vtcombo_process.wait_start() - - -def kill_vt_processes(): - """Call kill() on all processes.""" - logging.info('kill_vt_processes()') - if vtcombo_process: - vtcombo_process.kill() - - -def wait_vt_processes(): - """Call wait() on all processes.""" - logging.info('wait_vt_processes()') - if vtcombo_process: - vtcombo_process.wait() - - -def kill_and_wait_vt_processes(): - """Call kill() and then wait() on all processes.""" - kill_vt_processes() - wait_vt_processes() - - -# wait_step is a helper for looping until a condition is true. -# use as follow: -# timeout = 10 -# while True: -# if done: -# break -# timeout = utils.wait_step('condition', timeout) -def wait_step(msg, timeout, sleep_time=1.0): - timeout -= sleep_time - if timeout <= 0: - raise Exception("timeout waiting for condition '%s'" % msg) - logging.debug("Sleeping for %f seconds waiting for condition '%s'", - sleep_time, msg) - time.sleep(sleep_time) - return timeout